Compare commits

...

119 Commits

Author SHA1 Message Date
437ab8ccef fixed wrong indentations 2025-09-05 10:35:44 +09:00
714748d8e2 code touch-up for better maintainability 2025-08-29 10:44:05 +09:00
f5a95505a9 fixed the problem of not setting the log file properly 2025-08-24 21:18:07 +09:00
98966f85f6 enhanced the route resolution code further in server.go 2025-08-24 19:13:13 +09:00
c8cb71cf95 update the proxy code to resolve a route by the client token and the client-side peer name 2025-08-24 19:00:26 +09:00
e2a3180ec7 added the tls configuration for pxy targets 2025-08-24 14:36:10 +09:00
42ceb5f3fa enhanced the logger to use color on a tty 2025-08-21 22:48:28 +09:00
9f8d3e2696 added a event pipe to break unix.Poll better for local pty 2025-08-21 21:10:06 +09:00
7ec1387132 improved code to break unix.Poll() using a seperate pipe for rpty 2025-08-21 20:40:51 +09:00
7d3ce7147a updated client rpx logging to print the x-forwarded-host value 2025-08-21 16:01:26 +09:00
0d76146bc3 enhanced rpx message logging 2025-08-21 15:19:32 +09:00
1dcb1605b7 upated NewClient() to append the default port number of the rpx target address if the port number isn't found 2025-08-20 23:36:23 +09:00
6078a41504 moved some goroutines to separate functions for improved readability 2025-08-20 14:11:02 +09:00
0696f4f560 updated code to add x-forwarded-host and x-forwarded-proto for rpx 2025-08-20 02:04:06 +09:00
c001458b24 added the rpx_sessions counter 2025-08-19 22:23:22 +09:00
10c139e837 added code for rpx handling 2025-08-19 20:20:18 +09:00
31a4223aab minotr message fix 2025-08-12 16:40:18 +09:00
41cd725c1c added server-rpx.go 2025-08-12 16:30:56 +09:00
6200bc5460 removed UNUSED from the proto file 2025-08-12 16:29:44 +09:00
7fb4fbaae2 updated http logging to include the query string part 2025-08-12 12:17:33 +09:00
d818acc53d rpty at least working 2025-08-12 02:50:10 +09:00
05cb0823b4 some code clean-up in handling grpc packets 2025-08-10 17:23:01 +09:00
d0f1663bf3 renamed pts to pty to avoid name collision 2025-08-08 19:24:52 +09:00
3fd91b2c45 updated routing rules a bit 2025-07-30 18:23:23 +09:00
5767beb9af removed the /_pts/favicon.ico endpoint 2025-06-24 20:56:16 +09:00
6e670b4924 updated http endpoints for more consistent xterm.html access 2025-06-24 00:58:46 +09:00
8331fdc1a2 implemented the pts feature in the server side as well 2025-06-23 21:09:24 +09:00
d092540f08 merged xterm.html and xterm-pts.html
made relevant code changes oin the server side as well
2025-06-22 13:11:25 +09:00
7835696166 enhanced the websocket endpoints to return failure for normal https packets 2025-06-21 22:01:24 +09:00
c5bac71eaf some minor fixes 2025-06-21 10:54:51 +09:00
9addb5d35f specified some flags to yaml.NewDecoder() 2025-06-21 10:30:51 +09:00
e12cd28413 fixed a silly bug of passing null 2025-06-21 03:11:31 +09:00
6baf3b2b53 added pty terminal support to the client side 2025-06-21 02:43:28 +09:00
01eb2edd6e fixed nil pointer access in wpx api call 2025-05-28 16:03:31 +09:00
cea0efcb18 added --config-file-pattern to server and client 2025-05-28 09:33:29 +09:00
db9fc1f4d9 switching to use github.com/goccy/go-yaml 2025-05-27 23:34:47 +09:00
54e9e208f4 fixed the http access rule matching 2025-05-27 21:54:54 +09:00
f06433d431 changed ioutil.ReadFile to os.ReadFile 2025-05-19 16:29:52 +09:00
8318643735 changed the recipe name from test to check in Makefile 2025-05-18 11:40:04 +09:00
deb6f7b05a moved some waiting loops to goroutines to avoid race conditions 2025-05-12 19:08:54 +09:00
dee3711dd4 changed the address variable to print 2025-05-01 13:07:22 +09:00
be864129dc fixed the command to generate tls files 2025-04-08 23:39:50 +09:00
e1c288f17f some error output to stderr 2025-04-08 15:13:47 +09:00
a1f8d4cf22 minor relocation of a variable 2025-04-07 15:24:02 +09:00
9c3a4d0c17 changed the unit of lifetimestart to a millisecond
Updated to fire route updated upon lifetime extension
2025-04-03 12:30:29 +09:00
b41df682e1 added function to maintain the list of service addresses 2025-03-31 23:40:45 +09:00
918b887517 renamed server_ctl to ServerCtl and capitalized the first letter of inner fields for exposure 2025-03-29 13:29:02 +09:00
76cba687ed created event firing functions under server and client to replace directy bulleting enqueing 2025-03-28 17:03:17 +09:00
fd28add458 switch to use Atom for connect_ssh_cancel 2025-03-28 00:56:58 +09:00
0cfd241e00 added more client-side endpoints 2025-03-26 23:44:41 +09:00
5dbe9cd34e revised Makefile 2025-03-24 01:51:05 +09:00
494f11836c added another control endpoint /_ctl/server-conns/{conn_id}/peers 2025-03-23 13:22:53 +09:00
41503373d3 added a new endpoint - /_ctl/server-routes 2025-03-22 14:01:42 +09:00
e01a6b347c updated to support multiple notice handlers with a fix on race condition accessing cts.Token on the client side 2025-03-21 12:53:16 +09:00
8cde9f08d4 added /_ctl/server-peers 2025-03-19 00:24:42 +09:00
9865914436 fired CONN_UPDATED from receive_from_stream 2025-03-18 23:45:00 +09:00
5fa6cd466b added the routes parameter to the server-conns endpoint 2025-03-18 23:37:46 +09:00
3714138656 added fields to hold creation time in various structures 2025-03-14 22:51:23 +09:00
f1f74ed48d updated wrong fix on waitgroup in the previous commit as well as a type of a field name of ServerEventConnDeleted 2025-03-13 22:59:38 +09:00
cd32380425 added Atom[T] to have atomic manipulation of composite values 2025-03-13 21:24:59 +09:00
4d3fb7db65 partial authentication in ctl websocket 2025-03-13 09:43:17 +09:00
8105545e98 use s.WrapWebsocketHandler for ssh websockets 2025-03-12 12:57:46 +09:00
1e6fbed19d fixed wrong queue implementation in bulletin.go 2025-03-12 12:08:56 +09:00
b398816c96 added Enqueue() and Dequeue() to bulltin.go
thinking to remove topics but still retained
2025-03-12 03:00:28 +09:00
09593fd678 fixed some potential concurrency issues in the client side.
enhanded route_started log messages
renamed service to svc for some items
added two more fields to denote requested service address/network to ServerRoute
2025-03-11 21:12:05 +09:00
befe65b486 enhanced the bulletin code 2025-03-10 19:56:14 +09:00
030d62af12 writing bulletin subscription/publish code 2025-03-10 09:33:19 +09:00
ae13d0c4ed starting simple messaging code 2025-03-08 15:17:27 +09:00
7b1d383813 renamed ReportEvent to ReportPacket 2025-03-08 11:34:05 +09:00
6e9887f726 updated the notice endpoint to access the 'text' field in the payload for convenience 2025-03-07 23:56:30 +09:00
ecc1d4580f removed redundant endding semicolons 2025-03-07 21:12:21 +09:00
e56c45b3bf updated to return connections, routes, peers in ascending order by id over the control endpoint 2025-03-07 13:41:44 +09:00
b6fb296608 added close-on-conn-error-event.
added the client token in connection output over ctl
2025-03-05 00:44:00 +09:00
bec93289f5 added the CONN_ERROR event t ype 2025-03-04 01:16:46 +09:00
04e2de609e updated the response code to MethodDelete from Ok To NoContent 2025-02-28 17:26:46 +09:00
71a42af593 added a debugging log message for conn_notice packets
created SendNotice function for reuse by external parties
2025-02-26 19:45:29 +09:00
5df95159a3 exposed the cfg field by nameing cfg to Cfg in server.go 2025-02-26 14:46:09 +09:00
2d63e81e62 renamed wrap_http_handler to WrapHttpHandler for server 2025-02-26 14:28:41 +09:00
97885bcae1 fixed an issue of missing parameters to fmt.Errorf()
fixed the name composition for prometheus
2025-02-24 10:34:22 +09:00
5c2695e46b ongoing code refactoring to use common functions 2025-02-23 20:06:37 +09:00
75f72e7c88 slightly more reuse of a common function 2025-02-23 00:55:15 +09:00
7363986737 refactored functions in client-ctl.go to use common functions 2025-02-22 10:08:57 +09:00
429bb6cd63 enhanced func (s *Server) FindServerConnByIdStr(conn_id string) to treat non-numeric conn_id as a connection token 2025-02-20 23:12:16 +09:00
d9aaa0a0ab added code dealing with client connection token 2025-02-20 22:21:39 +09:00
7a6b820b92 added some todo text 2025-02-20 01:27:22 +09:00
c7b7bfd25f added theConnDesc message to the grpc protocol 2025-02-20 00:59:00 +09:00
1c49023c37 capatalized the first letter of some field name for exposure outside package 2025-02-19 17:17:53 +09:00
b5c1ae2a73 enhanced the reconnect logic in client.go 2025-02-19 10:38:23 +09:00
2b3a841299 added default notice handling interface 2025-02-18 14:44:45 +09:00
81f7bb0c0d added /_ctl/client-conns/id/nocies 2025-02-18 01:17:51 +09:00
a0efb55c3e trying to support notice event 2025-02-18 01:02:26 +09:00
be7f4f4da5 stored client-side peer info to ServerConnPeer when PEER_STARTED is received 2025-02-17 00:52:29 +09:00
f2536a0acc added code dealing with server-side peers 2025-02-15 19:44:48 +09:00
cb18a44cfa started writing a new endpoint for server peer info 2025-02-15 18:06:10 +09:00
fb465133b9 minor code update 2025-02-14 16:18:57 +09:00
d5108e9859 allowed to bypass authentication for a specific endpoint to be accessed from ssh client 2025-02-14 12:45:54 +09:00
3dc5d9c91e updated to support cors in primitive manner 2025-02-10 14:48:18 +09:00
ec51c101ec updated the authentication to recognize X-Auth-Username and X-Auth-Password 2025-02-04 01:30:19 +09:00
ef3e80efb8 preallocated of access rules instead of using append() 2025-02-02 09:57:43 +09:00
1bc8406907 fixed a bug in handling the log file argument 2025-02-01 00:19:47 +09:00
0fb57cb77b added http auth config to the client-side control channel 2025-02-01 00:06:05 +09:00
16327fc576 updated to code to enforce access rules to the control channel 2025-01-31 21:20:10 +09:00
148dfbcfe1 updated Authenticate to return status code 2025-01-31 19:54:28 +09:00
8bee855aa8 added code for token issuance and verification 2025-01-31 04:06:03 +09:00
b7992a0bb7 renamed BasicAuth to Auth 2025-01-29 00:30:08 +09:00
2fa5817e88 added some code for control channel authentication 2025-01-28 23:50:28 +09:00
a97be385ec some ground work to support authentcation on the control channel 2025-01-28 12:43:03 +09:00
d3afe29d5a combining server configuration items to a single structure 2025-01-28 02:35:11 +09:00
2655da937f renamed ssh_pxy_sessions to pxy_ssh_sessions 2025-01-28 01:29:21 +09:00
c5f63328b2 added code to export metrics 2025-01-28 00:44:02 +09:00
810356efe5 added the State field to the ClientConn structure 2025-01-22 10:46:22 +09:00
90305e3eed added the Static field to the ClientRoute structure 2025-01-19 12:17:27 +09:00
cfc0db4b54 renamed parse_duration_string to ParseDurationString
added DurationToSecString()
2025-01-19 01:56:31 +09:00
0809f9bedc renamed cts.id/sid to cts.Id/Sid respectively 2025-01-18 13:32:42 +09:00
e2f1d58c5e exposed some fields of the ClientRoute struct 2025-01-18 12:58:17 +09:00
edda6d169b changed the logger identifier for client 2025-01-17 14:47:33 +09:00
9118acb268 removed the extra stats table 2025-01-17 00:20:38 +09:00
369a41eeb2 renamed json_errmsg to JsonErrmsg 2025-01-16 09:32:06 +09:00
c237b8a842 updated code to treat ipv4inv6 as ipv4 2025-01-16 01:26:58 +09:00
33 changed files with 8722 additions and 1816 deletions

View File

@ -9,17 +9,26 @@ NAME=hodu
VERSION=1.0.0 VERSION=1.0.0
SRCS=\ SRCS=\
atom.go \
bulletin.go \
client.go \ client.go \
client-ctl.go \ client-ctl.go \
client-metrics.go \
client-peer.go \ client-peer.go \
client-pty.go \
hodu.go \ hodu.go \
hodu.pb.go \ hodu.pb.go \
hodu_grpc.pb.go \ hodu_grpc.pb.go \
jwt.go \
packet.go \ packet.go \
pty.go \
server.go \ server.go \
server-ctl.go \ server-ctl.go \
server-metrics.go \
server-peer.go \ server-peer.go \
server-proxy.go \ server-pty.go \
server-pxy.go \
server-rpx.go \
system.go \ system.go \
transform.go \ transform.go \
@ -30,6 +39,7 @@ DATA = \
xterm.html xterm.html
CMD_DATA=\ CMD_DATA=\
cmd/rsa.key \
cmd/tls.crt \ cmd/tls.crt \
cmd/tls.key cmd/tls.key
@ -41,13 +51,19 @@ CMD_SRCS=\
all: $(NAME) all: $(NAME)
$(NAME): $(DATA) $(SRCS) $(CMD_DATA) $(CMD_SRCS) $(NAME): $(DATA) $(SRCS) $(CMD_DATA) $(CMD_SRCS)
##CGO_ENABLED=0 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)'" -o $@ $(CMD_SRCS) CGO_ENABLED=0 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)'" -o $@ $(CMD_SRCS)
CGO_ENABLED=1 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)'" -o $@ $(CMD_SRCS) ##CGO_ENABLED=1 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)'" -o $@ $(CMD_SRCS)
##CGO_ENABLED=1 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)' -linkmode external -extldflags=-static" -o $@ $(CMD_SRCS) ##CGO_ENABLED=1 go build -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)' -linkmode external -extldflags=-static" -o $@ $(CMD_SRCS)
$(NAME).debug: $(DATA) $(SRCS) $(CMD_DATA) $(CMD_SRCS)
CGO_ENABLED=1 go build -race -x -ldflags "-X 'main.HODU_NAME=$(NAME)' -X 'main.HODU_VERSION=$(VERSION)'" -o $@ $(CMD_SRCS)
clean: clean:
go clean -x -i go clean -x -i
rm -f $(NAME) rm -f $(NAME) $(NAME).debug
check:
go test -x
hodu.pb.go: hodu.proto hodu.pb.go: hodu.proto
protoc --go_out=. --go_opt=paths=source_relative \ protoc --go_out=. --go_opt=paths=source_relative \
@ -70,9 +86,12 @@ xterm.css:
sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@" sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@"
cmd/tls.crt: cmd/tls.crt:
openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:10.0.0.1,IP:::1" openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:127.0.0.1,IP:::1"
cmd/tls.key: cmd/tls.key:
openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:10.0.0.1,IP:::1" openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:127.0.0.1,IP:::1"
.PHONY: clean cmd/rsa.key:
openssl genrsa -traditional -out cmd/rsa.key 2048
.PHONY: all clean test

View File

@ -1,7 +1,7 @@
## normal operation ## normal operation
- ./hodu server --rpc-on=0.0.0.0:9999 --ctl-on=0.0.0.0:8888 --pxy-on=0.0.0.0:9998 --wpx-on=0.0.0.0:9997 - ./hodu server --rpc-on=0.0.0.0:9999 --ctl-on=0.0.0.0:8888 --pxy-on=0.0.0.0:9998 --wpx-on=0.0.0.0:9997
- ./hodu client --rpc-to=127.0.0.1:9999 --ctl-on=127.0.0.1:7777 "127.0.0.2:22,0.0.0.0:12345,ssh,Access SSH Server" - ./hodu client --rpc-to=127.0.0.1:9999 --ctl-on=127.0.0.1:1107 "127.0.0.2:22,0.0.0.0:12345,ssh,Access SSH Server"
## server.json ## server.json
``` ```
@ -29,13 +29,13 @@
"client-peer-addr": "192.168.1.104:22", "client-peer-addr": "192.168.1.104:22",
"client-peer-name": "Star gate", "client-peer-name": "Star gate",
"server-peer-option": "tcp4 ssh", "server-peer-option": "tcp4 ssh",
"server-peer-service-addr": "0.0.0.0:0", "server-peer-svc-addr": "0.0.0.0:0",
"server-peer-service-net": "", "server-peer-svc-net": "",
"lifetime": "0" "lifetime": "0"
} }
``` ```
Run this command: Run this command:
``` ```
curl -X POST --data-binary @client-route.json http://127.0.0.1:7777/_ctl/client-conns/1/routes curl -X POST --data-binary @client-route.json http://127.0.0.1:1107/_ctl/client-conns/1/routes
``` ```

21
atom.go Normal file
View File

@ -0,0 +1,21 @@
package hodu
import "sync/atomic"
type Atom[T any] struct {
val atomic.Value
}
func (av* Atom[T]) Set(v T) {
av.val.Store(v)
}
func (av* Atom[T]) Get() T {
var v interface{}
v = av.val.Load()
if v == nil {
var t T
return t // return the zero-value
}
return v.(T)
}

282
bulletin.go Normal file
View File

@ -0,0 +1,282 @@
package hodu
import "container/list"
import "container/ring"
import "errors"
import "sync"
import "time"
type BulletinSubscription[T interface{}] struct {
C chan T
b *Bulletin[T]
topic string
node *list.Element
}
type BulletinSubscriptionList = *list.List
type BulletinSubscriptionMap map[string]BulletinSubscriptionList
type Bulletin[T interface{}] struct {
svc Service
sbsc_map BulletinSubscriptionMap
sbsc_list *list.List
sbsc_mtx sync.RWMutex
blocked bool
r_mtx sync.RWMutex
r *ring.Ring
r_head *ring.Ring
r_tail *ring.Ring
r_len int
r_cap int
r_chan chan struct{}
stop_chan chan struct{}
}
func NewBulletin[T interface{}](svc Service, capa int) *Bulletin[T] {
var r *ring.Ring
r = ring.New(capa)
return &Bulletin[T]{
sbsc_map: make(BulletinSubscriptionMap, 0),
sbsc_list: list.New(),
r: r,
r_head: r,
r_tail: r,
r_cap: capa,
r_len: 0,
r_chan: make(chan struct{}, 1),
stop_chan: make(chan struct{}, 1),
}
}
func (b *Bulletin[T]) unsubscribe_list_nolock(sl BulletinSubscriptionList) {
var sbsc *BulletinSubscription[T]
var e *list.Element
for e = sl.Front(); e != nil; e = e.Next() {
sbsc = e.Value.(*BulletinSubscription[T])
sl.Remove(sbsc.node)
close(sbsc.C)
sbsc.b = nil
sbsc.node = nil
}
}
func (b *Bulletin[T]) unsubscribe_all_nolock() {
var topic string
var sl BulletinSubscriptionList
for topic, sl = range b.sbsc_map {
b.unsubscribe_list_nolock(sl)
delete(b.sbsc_map, topic)
}
b.unsubscribe_list_nolock(b.sbsc_list)
b.blocked = true
}
func (b *Bulletin[T]) UnsubscribeAll() {
b.sbsc_mtx.Lock()
b.unsubscribe_all_nolock()
b.sbsc_mtx.Unlock()
}
func (b *Bulletin[T]) Block() {
b.sbsc_mtx.Lock()
b.blocked = true
b.sbsc_mtx.Unlock()
}
func (b *Bulletin[T]) Unblock() {
b.sbsc_mtx.Lock()
b.blocked = false
b.sbsc_mtx.Unlock()
}
func (b *Bulletin[T]) Subscribe(topic string) (*BulletinSubscription[T], error) {
var sbsc BulletinSubscription[T]
b.sbsc_mtx.Lock()
if b.blocked {
b.sbsc_mtx.Unlock()
return nil, errors.New("blocked")
}
sbsc.C = make(chan T, 128) // TODO: size?
sbsc.b = b
sbsc.topic = topic
if topic == "" {
sbsc.node = b.sbsc_list.PushBack(&sbsc)
} else {
var sbsc_list BulletinSubscriptionList
var ok bool
sbsc_list, ok = b.sbsc_map[topic]
if !ok {
sbsc_list = list.New()
b.sbsc_map[topic] = sbsc_list
}
sbsc.node = sbsc_list.PushBack(&sbsc)
}
b.sbsc_mtx.Unlock()
return &sbsc, nil
}
func (b *Bulletin[T]) Unsubscribe(sbsc *BulletinSubscription[T]) {
if sbsc.b == b && sbsc.node != nil {
var sl BulletinSubscriptionList
var ok bool
b.sbsc_mtx.Lock()
if sbsc.topic == "" {
b.sbsc_list.Remove(sbsc.node)
close(sbsc.C)
sbsc.node = nil
sbsc.b = nil
} else {
sl, ok = b.sbsc_map[sbsc.topic]
if ok {
sl.Remove(sbsc.node)
close(sbsc.C)
sbsc.node = nil
sbsc.b = nil
}
}
b.sbsc_mtx.Unlock()
}
}
func (b *Bulletin[T]) Publish(topic string, data T) {
var sl BulletinSubscriptionList
var ok bool
if topic == "" { return }
b.sbsc_mtx.Lock()
if b.blocked {
b.sbsc_mtx.Unlock()
return
}
sl, ok = b.sbsc_map[topic]
if ok {
var sbsc *BulletinSubscription[T]
var e *list.Element
for e = sl.Front(); e != nil; e = e.Next() {
sbsc = e.Value.(*BulletinSubscription[T])
select {
case sbsc.C <- data:
// ok. could be written.
default:
// channel full. discard it
}
}
}
b.sbsc_mtx.Unlock()
}
func (b *Bulletin[T]) Enqueue(data T) {
// hopefuly, it's fater to use a single mutex, a ring buffer, and a notification channel than
// to use a channel to pass messages. TODO: performance verification
b.r_mtx.Lock()
if b.blocked {
b.r_mtx.Unlock()
return
}
if b.r_len < b.r_cap {
b.r_len++
} else {
b.r_head = b.r_head.Next()
}
b.r_tail.Value = data // update the value at the current position
b.r_tail = b.r_tail.Next() // move the current position
select {
case b.r_chan <- struct{}{}:
// write success
default:
// don't care if not writable
}
b.r_mtx.Unlock()
}
func (b *Bulletin[T]) Dequeue() (T, bool) {
var v T
var ok bool
b.r_mtx.Lock()
if b.r_len > 0 {
v = b.r_head.Value.(T) // store the value for returning
b.r_head.Value = nil // nullify the value
b.r_head = b.r_head.Next() // advance the head position
b.r_len--
ok = true
}
b.r_mtx.Unlock()
return v, ok
}
func (b *Bulletin[T]) RunTask(wg *sync.WaitGroup) {
var done bool
var tmr *time.Timer
defer wg.Done()
tmr = time.NewTimer(3 * time.Second)
for !done {
var msg T
var ok bool
msg, ok = b.Dequeue()
if !ok {
select {
case <-b.stop_chan:
// this may break the loop prematurely while there
// are messages to read as it uses two different channels:
// one for stop, another for notification
done = true
case <-b.r_chan:
// noti received.
tmr.Stop()
tmr.Reset(3 * time.Second)
case <-tmr.C:
// try to dequeue again
tmr.Reset(3 * time.Second)
}
} else {
// forward msg to all subscribers...
var e *list.Element
var sbsc *BulletinSubscription[T]
tmr.Stop()
b.sbsc_mtx.Lock()
for e = b.sbsc_list.Front(); e != nil; e = e.Next() {
sbsc = e.Value.(*BulletinSubscription[T])
select {
case sbsc.C <- msg:
// ok. could be written.
default:
// channel full. discard it
}
}
b.sbsc_mtx.Unlock()
}
}
tmr.Stop()
}
func (b *Bulletin[T]) ReqStop() {
select {
case b.stop_chan <- struct{}{}:
// write success
default:
// ignore failure
}
}

139
bulletin_test.go Normal file
View File

@ -0,0 +1,139 @@
package hodu_test
import "fmt"
import "hodu"
import "sync"
import "testing"
import "time"
func TestBulletin1(t *testing.T) {
var b *hodu.Bulletin[string]
var s1 *hodu.BulletinSubscription[string]
var s2 *hodu.BulletinSubscription[string]
var wg sync.WaitGroup
var nmsgs1 int
var nmsgs2 int
b = hodu.NewBulletin[string](nil, 100)
s1, _ = b.Subscribe("t1")
s2, _ = b.Subscribe("t2")
wg.Add(1)
go func() {
var m string
var ok bool
var c1 chan string
var c2 chan string
c1 = s1.C
c2 = s2.C
defer wg.Done()
for c1 != nil || c2 != nil {
select {
case m, ok = <-c1:
if ok { fmt.Printf ("s1: %+v\n", m); nmsgs1++ } else { c1 = nil; fmt.Printf ("s1 closed\n")}
case m, ok = <-c2:
if ok { fmt.Printf ("s2: %+v\n", m); nmsgs2++ } else { c2 = nil; fmt.Printf ("s2 closed\n") }
}
}
}()
b.Publish("t1", "donkey")
b.Publish("t2", "monkey")
b.Publish("t1", "donkey kong")
b.Publish("t2", "monkey hong")
b.Publish("t3", "home")
b.Publish("t2", "fire")
b.Publish("t1", "sunflower")
b.Publish("t2", "itsy bitsy spider")
b.Publish("t3", "marigold")
b.Publish("t3", "parrot")
time.Sleep(100 * time.Millisecond)
b.Publish("t2", "tiger")
time.Sleep(100 * time.Millisecond)
b.Unsubscribe(s2)
b.Publish("t2", "lion king")
b.Publish("t2", "fly to the skyp")
time.Sleep(100 * time.Millisecond)
b.Block()
b.UnsubscribeAll()
wg.Wait()
fmt.Printf ("---------------------\n")
if nmsgs1 != 3 { t.Errorf("number of messages for s1 received must be 3, but got %d\n", nmsgs1) }
if nmsgs2 != 5 { t.Errorf("number of messages for s2 received must be 5, but got %d\n", nmsgs2) }
}
func TestBulletin2(t *testing.T) {
var b *hodu.Bulletin[string]
var s1 *hodu.BulletinSubscription[string]
var s2 *hodu.BulletinSubscription[string]
var wg sync.WaitGroup
var nmsgs1 int
var nmsgs2 int
b = hodu.NewBulletin[string](nil, 13) // if the size is too small, some messages are lost
wg.Add(1)
go b.RunTask(&wg)
s1, _ = b.Subscribe("")
s2, _ = b.Subscribe("")
wg.Add(1)
go func() {
var m string
var ok bool
var c1 chan string
var c2 chan string
c1 = s1.C
c2 = s2.C
defer wg.Done()
for c1 != nil || c2 != nil {
select {
case m, ok = <-c1:
if ok { fmt.Printf ("s1: %+v\n", m); nmsgs1++ } else { c1 = nil; fmt.Printf ("s1 closed\n")}
case m, ok = <-c2:
if ok { fmt.Printf ("s2: %+v\n", m); nmsgs2++ } else { c2 = nil; fmt.Printf ("s2 closed\n") }
}
}
}()
b.Enqueue("donkey")
b.Enqueue("monkey")
b.Enqueue("donkey kong")
b.Enqueue("monkey hong")
b.Enqueue("home")
b.Enqueue("fire")
b.Enqueue("sunflower")
b.Enqueue("itsy bitsy spider")
b.Enqueue("marigold")
b.Enqueue("parrot")
b.Enqueue("tiger")
b.Enqueue("walrus")
b.Enqueue("donkey runs")
// without this unsubscription may happen before s2.C can receive messages
// 100 millisconds must be longer than enough for all messages to be received
time.Sleep(100 * time.Millisecond)
b.Unsubscribe(s2)
b.Enqueue("lion king")
b.Enqueue("fly to the ground")
b.Enqueue("dig it")
b.Enqueue("dig it dawg")
time.Sleep(100 * time.Millisecond)
b.UnsubscribeAll()
b.ReqStop()
wg.Wait()
fmt.Printf ("---------------------\n")
if nmsgs1 != 17 { t.Errorf("number of messages for s1 received must be 17, but got %d\n", nmsgs1) }
if nmsgs2 != 13 { t.Errorf("number of messages for s2 received must be 13, but got %d\n", nmsgs2) }
}

File diff suppressed because it is too large Load Diff

126
client-metrics.go Normal file
View File

@ -0,0 +1,126 @@
package hodu
import "runtime"
import "strings"
import "github.com/prometheus/client_golang/prometheus"
type ClientCollector struct {
client *Client
BuildInfo *prometheus.Desc
ClientConns *prometheus.Desc
ClientRoutes *prometheus.Desc
ClientPeers *prometheus.Desc
PtySessions *prometheus.Desc
RptySessions *prometheus.Desc
RpxSessions *prometheus.Desc
}
// NewClientCollector returns a new ClientCollector with all prometheus.Desc initialized
func NewClientCollector(client *Client) ClientCollector {
var prefix string
// prometheus doesn't like a dash. change it to an underscore
prefix = strings.ReplaceAll(client.Name(), "-", "_") + "_"
return ClientCollector{
client: client,
BuildInfo: prometheus.NewDesc(
prefix + "build_info",
"Build information",
[]string{
"goarch",
"goos",
"goversion",
}, nil,
),
ClientConns: prometheus.NewDesc(
prefix + "client_conns",
"Number of client connections from clients",
nil, nil,
),
ClientRoutes: prometheus.NewDesc(
prefix + "client_routes",
"Number of client-side routes",
nil, nil,
),
ClientPeers: prometheus.NewDesc(
prefix + "client_peers",
"Number of client-side peers",
nil, nil,
),
PtySessions: prometheus.NewDesc(
prefix + "pty_sessions",
"Number of pty sessions",
nil, nil,
),
RptySessions: prometheus.NewDesc(
prefix + "rpty_sessions",
"Number of rpty sessions",
nil, nil,
),
RpxSessions: prometheus.NewDesc(
prefix + "rpx_sessions",
"Number of rpx sessions",
nil, nil,
),
}
}
func (c ClientCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.BuildInfo
ch <- c.ClientConns
ch <- c.ClientRoutes
ch <- c.ClientPeers
ch <- c.PtySessions
ch <- c.RptySessions
ch <- c.RpxSessions
}
func (c ClientCollector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
c.BuildInfo,
prometheus.GaugeValue,
1,
runtime.GOARCH,
runtime.GOOS,
runtime.Version(),
)
ch <- prometheus.MustNewConstMetric(
c.ClientConns,
prometheus.GaugeValue,
float64(c.client.stats.conns.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.ClientRoutes,
prometheus.GaugeValue,
float64(c.client.stats.routes.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.ClientPeers,
prometheus.GaugeValue,
float64(c.client.stats.peers.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.PtySessions,
prometheus.GaugeValue,
float64(c.client.stats.pty_sessions.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.RptySessions,
prometheus.GaugeValue,
float64(c.client.stats.rpty_sessions.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.RpxSessions,
prometheus.GaugeValue,
float64(c.client.stats.rpx_sessions.Load()),
)
}

View File

@ -26,34 +26,49 @@ func (cpc *ClientPeerConn) RunTask(wg *sync.WaitGroup) error {
var n int var n int
defer wg.Done() defer wg.Done()
cpc.route.cts.C.FirePeerEvent(CLIENT_EVENT_PEER_STARTED, cpc)
for { for {
n, err = cpc.conn.Read(buf[:]) n, err = cpc.conn.Read(buf[:])
if n > 0 {
var err2 error
err2 = cpc.route.cts.psc.Send(MakePeerDataPacket(cpc.route.Id, cpc.conn_id, buf[0:n]))
if err2 != nil {
cpc.route.cts.C.log.Write(cpc.route.cts.Sid, LOG_ERROR,
"Failed to write peer(%d,%d,%s,%s) data to server - %s",
cpc.route.Id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String(), err2.Error())
break
}
}
if err != nil { if err != nil {
if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") { // i hate checking this condition with strings.Contains() if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") { // i hate checking this condition with strings.Contains()
cpc.route.cts.cli.log.Write(cpc.route.cts.sid, LOG_INFO, cpc.route.cts.C.log.Write(cpc.route.cts.Sid, LOG_INFO,
"Client-side peer(%d,%d,%s,%s) closed", "Client-side peer(%d,%d,%s,%s) closed",
cpc.route.id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String()) cpc.route.Id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String())
} else { } else {
cpc.route.cts.cli.log.Write(cpc.route.cts.sid, LOG_ERROR, cpc.route.cts.C.log.Write(cpc.route.cts.Sid, LOG_ERROR,
"Failed to read from client-side peer(%d,%d,%s,%s) - %s", "Failed to read from client-side peer(%d,%d,%s,%s) - %s",
cpc.route.id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String(), err.Error()) cpc.route.Id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String(), err.Error())
} }
break break
} }
err = cpc.route.cts.psc.Send(MakePeerDataPacket(cpc.route.id, cpc.conn_id, buf[0:n]))
if err != nil {
cpc.route.cts.cli.log.Write(cpc.route.cts.sid, LOG_ERROR,
"Failed to write peer(%d,%d,%s,%s) data to server - %s",
cpc.route.id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String(), err.Error())
break
}
} }
cpc.route.cts.psc.Send(MakePeerStoppedPacket(cpc.route.id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String())) // nothing much to do upon failure. no error check here cpc.route.cts.psc.Send(MakePeerStoppedPacket(cpc.route.Id, cpc.conn_id, cpc.conn.RemoteAddr().String(), cpc.conn.LocalAddr().String())) // nothing much to do upon failure. no error check here
cpc.ReqStop() cpc.ReqStop()
cpc.route.RemoveClientPeerConn(cpc) cpc.route.RemoveClientPeerConn(cpc)
cpc.route.cts.C.ptc_mtx.Lock()
cpc.route.cts.C.ptc_list.Remove(cpc.node_in_client)
cpc.node_in_client = nil
cpc.route.cts.C.ptc_mtx.Unlock()
cpc.route.cts.ptc_mtx.Lock()
cpc.route.cts.ptc_list.Remove(cpc.node_in_conn)
cpc.node_in_conn = nil
cpc.route.cts.ptc_mtx.Unlock()
cpc.route.cts.C.FirePeerEvent(CLIENT_EVENT_PEER_STOPPED, cpc)
return nil return nil
} }

299
client-pty.go Normal file
View File

@ -0,0 +1,299 @@
package hodu
import "encoding/json"
import "errors"
import "io"
import "net/http"
import "os"
import "os/exec"
import "strconv"
import "strings"
import "sync"
import "text/template"
import pts "github.com/creack/pty"
import "golang.org/x/net/websocket"
import "golang.org/x/sys/unix"
type client_pty_ws struct {
C *Client
Id string
ws *websocket.Conn
}
type client_pty_xterm_file struct {
client_ctl
file string
}
// ------------------------------------------------------
func (pty *client_pty_ws) Identity() string {
return pty.Id
}
func (pty *client_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
var c *Client
var req *http.Request
//var username string
//var password string
var in *os.File
var out *os.File
var tty *os.File
var cmd *exec.Cmd
var pfd [2]int = [2]int{ -1, -1 }
var wg sync.WaitGroup
var conn_ready_chan chan bool
var err error
c = pty.C
req = ws.Request()
conn_ready_chan = make(chan bool, 3)
wg.Add(1)
go func() {
var conn_ready bool
defer wg.Done()
defer ws.Close() // dirty way to break the main loop
conn_ready = <-conn_ready_chan
if conn_ready { // connected
var poll_fds []unix.PollFd
var buf [2048]byte
var n int
var err error
poll_fds = []unix.PollFd{
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
}
c.stats.pty_sessions.Add(1)
for {
n, err = unix.Poll(poll_fds, -1) // -1 means wait indefinitely
if err != nil {
if errors.Is(err, unix.EINTR) { continue }
c.log.Write("", LOG_ERROR, "[%s] Failed to poll pty stdout - %s", req.RemoteAddr, err.Error())
break
}
if n == 0 { // timed out
continue
}
if (poll_fds[0].Revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
c.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty stdout", req.RemoteAddr)
break
}
if (poll_fds[1].Revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
c.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty event pipe", req.RemoteAddr)
break
}
if (poll_fds[0].Revents & unix.POLLIN) != 0 {
n, err = out.Read(buf[:])
if n > 0 {
var err2 error
err2 = send_ws_data_for_xterm(ws, "iov", string(buf[:n]))
if err2 != nil {
c.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to send to websocket - %s", req.RemoteAddr, err2.Error())
break
}
}
if err != nil {
if !errors.Is(err, io.EOF) {
c.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to read pty stdout - %s", req.RemoteAddr, err.Error())
}
break
}
}
if (poll_fds[1].Revents & unix.POLLIN) != 0 {
c.log.Write(pty.Id, LOG_DEBUG, "[%s] Stop request noticed on pty event pipe", req.RemoteAddr)
break
}
}
c.stats.pty_sessions.Add(-1)
}
}()
ws_recv_loop:
for {
var msg []byte
err = websocket.Message.Receive(ws, &msg)
if err != nil { goto done }
if len(msg) > 0 {
var ev json_xterm_ws_event
err = json.Unmarshal(msg, &ev)
if err == nil {
switch ev.Type {
case "open":
if tty == nil && len(ev.Data) == 2 {
// not using username and password for now...
//username = string(ev.Data[0])
//password = string(ev.Data[1])
wg.Add(1)
go func() {
var err error
defer wg.Done()
err = unix.Pipe(pfd[:])
if err != nil {
c.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to create event pipe for pty - %s", req.RemoteAddr, err.Error())
send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error
return
}
cmd, tty, err = connect_pty(c.pty_shell, c.pty_user)
if err != nil {
c.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to connect pty - %s", req.RemoteAddr, err.Error())
send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error
unix.Close(pfd[0]); pfd[0] = -1
unix.Close(pfd[1]); pfd[1] = -1
return
}
err = send_ws_data_for_xterm(ws, "status", "opened")
if err != nil {
c.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to write opened event to websocket - %s", req.RemoteAddr, err.Error())
ws.Close() // dirty way to flag out the error
unix.Close(pfd[0]); pfd[0] = -1
unix.Close(pfd[1]); pfd[1] = -1
return
}
c.log.Write(pty.Id, LOG_DEBUG, "[%s] Opened pty session", req.RemoteAddr)
out = tty
in = tty
conn_ready_chan <- true
}()
}
case "close":
if tty != nil {
tty.Close()
tty = nil
}
if pfd[1] >= 0 {
unix.Write(pfd[1], []byte{0})
}
break ws_recv_loop
case "iov":
if tty != nil {
var i int
for i, _ = range ev.Data {
in.Write([]byte(ev.Data[i]))
}
}
case "size":
if tty != nil && len(ev.Data) == 2 {
var rows int
var cols int
rows, _ = strconv.Atoi(ev.Data[0])
cols, _ = strconv.Atoi(ev.Data[1])
pts.Setsize(tty, &pts.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
c.log.Write(pty.Id, LOG_DEBUG, "[%s] Resized terminal to %d,%d", req.RemoteAddr, rows, cols)
// ignore error
}
}
}
}
}
if tty != nil {
err = send_ws_data_for_xterm(ws, "status", "closed")
if err != nil { goto done }
}
done:
conn_ready_chan <- false
ws.Close()
if cmd != nil {
// kill the child process underneath to close ptym(the master pty).
//cmd.Process.Signal(syscall.SIGTERM)
cmd.Process.Kill()
}
if tty != nil { tty.Close() }
if cmd != nil { cmd.Wait() }
wg.Wait()
// close the event pipe after all goroutines are over
if pfd[0] >= 0 { unix.Close(pfd[0]) }
if pfd[1] >= 0 { unix.Close(pfd[1]) }
c.log.Write(pty.Id, LOG_DEBUG, "[%s] Ended pty session", req.RemoteAddr)
return http.StatusOK, err
}
// ------------------------------------------------------
func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var c *Client
var status_code int
var err error
c = pty.c
switch pty.file {
case "xterm.js":
status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_js)
case "xterm-addon-fit.js":
status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_addon_fit_js)
case "xterm.css":
status_code = WriteCssRespHeader(w, http.StatusOK)
w.Write(xterm_css)
case "xterm.html":
var tmpl *template.Template
tmpl = template.New("")
if c.xterm_html != "" {
_, err = tmpl.Parse(c.xterm_html)
} else {
_, err = tmpl.Parse(xterm_html)
}
if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError)
goto oops
} else {
status_code = WriteHtmlRespHeader(w, http.StatusOK)
tmpl.Execute(w,
&xterm_session_info{
Mode: "pty",
ConnId: "-1",
RouteId: "-1",
})
}
case "_forbidden":
status_code = WriteEmptyRespHeader(w, http.StatusForbidden)
case "_notfound":
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
default:
if strings.HasPrefix(pty.file, "_redir:") {
status_code = http.StatusMovedPermanently
w.Header().Set("Location", pty.file[7:])
w.WriteHeader(status_code)
} else {
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
}
}
//done:
return status_code, nil
oops:
return status_code, err
}

2155
client.go

File diff suppressed because it is too large Load Diff

24
client_test.go Normal file
View File

@ -0,0 +1,24 @@
package hodu_test
import "context"
import "hodu"
import "testing"
type TestLogger struct {}
func (l *TestLogger) Write(id string, level hodu.LogLevel, fmtstr string, args ...interface{}) {}
func (l *TestLogger) WriteWithCallDepth(id string, level hodu.LogLevel, call_depth int, fmtstr string, args ...interface{}) {}
func (l *TestLogger) Rotate() {}
func (l *TestLogger) Close() {}
func TestClient001(t *testing.T) {
var c *hodu.Client
var r *hodu.ClientRoute
var err error
c = hodu.NewClient(context.Background(), "test-client", &TestLogger{}, &hodu.ClientConfig{})
r, err = c.FindClientRouteByServerPeerSvcPortIdStr("100", "200")
if err == nil { t.Errorf("Search on empty client structure must have failed") }
if r != nil { t.Errorf("Main route must not be nil upon no error") }
}

View File

@ -1,16 +1,20 @@
package main package main
import "crypto/rsa"
import "crypto/tls" import "crypto/tls"
import "crypto/x509" import "crypto/x509"
import "errors" import "encoding/base64"
import "encoding/pem"
import "fmt" import "fmt"
import "hodu" import "hodu"
import "io" import "net/netip"
import "io/ioutil"
import "os" import "os"
import "strings"
import "time" import "time"
import "gopkg.in/yaml.v3" //import "gopkg.in/yaml.v3"
import yaml "github.com/goccy/go-yaml"
type ServerTLSConfig struct { type ServerTLSConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
@ -42,9 +46,37 @@ type ClientTLSConfig struct {
ServerName string `yaml:"server-name"` ServerName string `yaml:"server-name"`
} }
type HttpAccessRule struct {
Prefix string `yaml:"prefix"`
OrgNets []string `yaml:"origin-networks"`
Action string `yaml:"action"`
}
type HttpAuthConfig struct {
Enabled bool `yaml:"enabled"`
Realm string `yaml:"realm"`
Creds []string `yaml:"credentials"`
TokenTtl string `yaml:"token-ttl"`
TokenRsaKeyText string `yaml:"token-rsa-key-text"`
TokenRsaKeyFile string `yaml:"token-rsa-key-file"`
AccessRules []HttpAccessRule `yaml:"access-rules"`
}
type CTLServiceConfig struct { type CTLServiceConfig struct {
Prefix string `yaml:"prefix"` // url prefix for control channel endpoints Prefix string `yaml:"prefix"` // url prefix for control channel endpoints
Addrs []string `yaml:"addresses"` Addrs []string `yaml:"addresses"`
Cors bool `yaml:"cors"`
Auth HttpAuthConfig `yaml:"auth"`
}
type RPXServiceConfig struct {
Addrs []string `yaml:"addresses"`
}
type RPXClientTokenConfig struct {
AttrName string `yaml:"attr-name"`
Regex string `yaml:"regex"`
SubmatchIndex int `yaml:"submatch-index"`
} }
type PXYServiceConfig struct { type PXYServiceConfig struct {
@ -72,6 +104,8 @@ type ServerAppConfig struct {
LogRotate int `yaml:"log-rotate"` LogRotate int `yaml:"log-rotate"`
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections
PtyUser string `yaml:"pty-user"`
PtyShell string `yaml:"pty-shell"`
XtermHtmlFile string `yaml:"xterm-html-file"` XtermHtmlFile string `yaml:"xterm-html-file"`
} }
@ -83,6 +117,11 @@ type ClientAppConfig struct {
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections
PeerConnTmout time.Duration `yaml:"peer-conn-timeout"` PeerConnTmout time.Duration `yaml:"peer-conn-timeout"`
TokenText string `yaml:"token-text"`
TokenFile string `yaml:"token-file"`
PtyUser string `yaml:"pty-user"`
PtyShell string `yaml:"pty-shell"`
XtermHtmlFile string `yaml:"xterm-html-file"`
} }
type ServerConfig struct { type ServerConfig struct {
@ -93,9 +132,18 @@ type ServerConfig struct {
TLS ServerTLSConfig `yaml:"tls"` TLS ServerTLSConfig `yaml:"tls"`
} `yaml:"ctl"` } `yaml:"ctl"`
RPX struct {
Service RPXServiceConfig `yaml:"service"`
TLS ServerTLSConfig `yaml:"tls"`
ClientToken RPXClientTokenConfig `yaml:"client-token"`
} `yaml:"rpx"`
PXY struct { PXY struct {
Service PXYServiceConfig `yaml:"service"` Service PXYServiceConfig `yaml:"service"`
TLS ServerTLSConfig `yaml:"tls"` TLS ServerTLSConfig `yaml:"tls"`
Target struct {
TLS ClientTLSConfig `yaml:"tls"`
} `yaml:"target"`
} `yaml:"pxy"` } `yaml:"pxy"`
WPX struct { WPX struct {
@ -120,48 +168,40 @@ type ClientConfig struct {
Endpoint RPCEndpointConfig `yaml:"endpoint"` Endpoint RPCEndpointConfig `yaml:"endpoint"`
TLS ClientTLSConfig `yaml:"tls"` TLS ClientTLSConfig `yaml:"tls"`
} `yaml:"rpc"` } `yaml:"rpc"`
RPX struct {
Target struct {
Addr string `yaml:"address"`
TLS ClientTLSConfig `yaml:"tls"`
} `yaml:"target"`
}
} }
func load_server_config(cfgfile string) (*ServerConfig, error) { func load_server_config_to(cfgfile string, cfg *ServerConfig) error {
var cfg ServerConfig
var f *os.File var f *os.File
var yd *yaml.Decoder var yd *yaml.Decoder
var err error var err error
f, err = os.Open(cfgfile) f, err = os.Open(cfgfile)
if err != nil && errors.Is(err, io.EOF) { if err != nil { return err }
return nil, err
}
yd = yaml.NewDecoder(f) yd = yaml.NewDecoder(f, yaml.AllowDuplicateMapKey(), yaml.DisallowUnknownField())
err = yd.Decode(&cfg) err = yd.Decode(cfg)
f.Close() f.Close()
if err != nil { return err
return nil, err
}
return &cfg, nil
} }
func load_client_config(cfgfile string) (*ClientConfig, error) { func load_client_config_to(cfgfile string, cfg *ClientConfig) error {
var cfg ClientConfig
var f *os.File var f *os.File
var yd *yaml.Decoder var yd *yaml.Decoder
var err error var err error
f, err = os.Open(cfgfile) f, err = os.Open(cfgfile)
if err != nil && errors.Is(err, io.EOF) { if err != nil { return err }
return nil, err
}
yd = yaml.NewDecoder(f) yd = yaml.NewDecoder(f, yaml.AllowDuplicateMapKey(), yaml.DisallowUnknownField())
err = yd.Decode(&cfg) err = yd.Decode(cfg)
f.Close() f.Close()
if err != nil { return err
return nil, err
}
return &cfg, nil
} }
@ -237,7 +277,7 @@ func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
cert, err = tls.X509KeyPair(hodu_tls_cert_text, hodu_tls_key_text) cert, err = tls.X509KeyPair(hodu_tls_cert_text, hodu_tls_key_text)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load key pair - %s", err) return nil, fmt.Errorf("failed to load key pair - %s", err.Error())
} }
cert_pool = x509.NewCertPool() cert_pool = x509.NewCertPool()
@ -248,7 +288,7 @@ func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
} }
} else if cfg.ClientCACertFile != "" { } else if cfg.ClientCACertFile != "" {
var text []byte var text []byte
text, err = ioutil.ReadFile(cfg.ClientCACertFile) text, err = os.ReadFile(cfg.ClientCACertFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load ca certficate file %s - %s", cfg.ClientCACertFile, err.Error()) return nil, fmt.Errorf("failed to load ca certficate file %s - %s", cfg.ClientCACertFile, err.Error())
} }
@ -295,7 +335,7 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
cert, err = tls.X509KeyPair(hodu_tls_cert_text, hodu_tls_key_text) cert, err = tls.X509KeyPair(hodu_tls_cert_text, hodu_tls_key_text)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load key pair - %s", err) return nil, fmt.Errorf("failed to load key pair - %s", err.Error())
} }
cert_pool = x509.NewCertPool() cert_pool = x509.NewCertPool()
@ -306,7 +346,7 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
} }
} else if cfg.ServerCACertFile != "" { } else if cfg.ServerCACertFile != "" {
var text []byte var text []byte
text, err = ioutil.ReadFile(cfg.ServerCACertFile) text, err = os.ReadFile(cfg.ServerCACertFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load ca certficate file %s - %s", cfg.ServerCACertFile, err.Error()) return nil, fmt.Errorf("failed to load ca certficate file %s - %s", cfg.ServerCACertFile, err.Error())
} }
@ -334,3 +374,99 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
return tlscfg, nil return tlscfg, nil
} }
// --------------------------------------------------------------------
func make_http_auth_config(cfg *HttpAuthConfig) (*hodu.HttpAuthConfig, error) {
var config hodu.HttpAuthConfig
var cred string
var b []byte
var x []string
var rsa_key_text []byte
var rk *rsa.PrivateKey
var pb *pem.Block
var rule HttpAccessRule
var idx int
var err error
config.Enabled = cfg.Enabled
config.Realm = cfg.Realm
config.Creds = make(hodu.HttpAuthCredMap)
config.TokenTtl, err = hodu.ParseDurationString(cfg.TokenTtl)
if err != nil {
return nil, fmt.Errorf("invalid token ttl %s - %s", cred, err.Error())
}
// convert user credentials
for _, cred = range cfg.Creds {
b, err = base64.StdEncoding.DecodeString(cred)
if err == nil { cred = string(b) }
// each entry must be of the form username:password
x = strings.Split(cred, ":")
if len(x) != 2 {
return nil, fmt.Errorf("invalid auth credential - %s", cred)
}
config.Creds[x[0]] = x[1]
}
// load rsa key
if cfg.TokenRsaKeyText == "" && cfg.TokenRsaKeyFile != "" {
rsa_key_text, err = os.ReadFile(cfg.TokenRsaKeyFile)
if err != nil {
return nil, fmt.Errorf("unable to read %s - %s", cfg.TokenRsaKeyFile, err.Error())
}
}
if len(rsa_key_text) == 0 { rsa_key_text = []byte(cfg.TokenRsaKeyText) }
if len(rsa_key_text) == 0 { rsa_key_text = hodu_rsa_key_text }
pb, b = pem.Decode(rsa_key_text)
if pb == nil || len(b) > 0 {
return nil, fmt.Errorf("invalid token rsa key text %s - no block or too many blocks", string(rsa_key_text))
}
rk, err = x509.ParsePKCS1PrivateKey(pb.Bytes)
if err != nil {
return nil, fmt.Errorf("invalid token rsa key text %s - %s", string(rsa_key_text), err.Error())
}
config.TokenRsaKey = rk
// load access rules
config.AccessRules = make([]hodu.HttpAccessRule, len(cfg.AccessRules))
for idx, rule = range cfg.AccessRules {
var action hodu.HttpAccessAction
var orgnet string
var orgnet_idx int
if rule.Prefix == "" {
return nil, fmt.Errorf("blank access rule prefix not allowed")
}
switch strings.ToLower(rule.Action) {
case "accept":
action = hodu.HTTP_ACCESS_ACCEPT
case "reject":
action = hodu.HTTP_ACCESS_REJECT
case "auth-required":
action = hodu.HTTP_ACCESS_AUTH_REQUIRED
default:
return nil, fmt.Errorf("invalid access rule action %s", rule.Action)
}
config.AccessRules[idx] = hodu.HttpAccessRule{
Prefix: rule.Prefix,
Action: action,
OrgNets: make([]netip.Prefix, len(rule.OrgNets)),
}
for orgnet_idx, orgnet = range rule.OrgNets {
var netpfx netip.Prefix
netpfx, err = netip.ParsePrefix(orgnet)
if err != nil { return nil, fmt.Errorf("invalid network %s - %s", orgnet, err.Error()) }
config.AccessRules[idx].OrgNets[orgnet_idx] = netpfx
}
}
return &config, nil
}

View File

@ -9,6 +9,7 @@ import "runtime"
import "strings" import "strings"
import "sync" import "sync"
import "sync/atomic" import "sync/atomic"
import "syscall"
import "time" import "time"
type app_logger_msg_t struct { type app_logger_msg_t struct {
@ -28,16 +29,44 @@ type AppLogger struct {
msg_chan chan app_logger_msg_t msg_chan chan app_logger_msg_t
wg sync.WaitGroup wg sync.WaitGroup
use_color bool
closed atomic.Bool closed atomic.Bool
} }
func NewAppLogger (id string, w io.Writer, mask hodu.LogMask) *AppLogger { func _is_ansi_tty(fd uintptr) bool {
var st syscall.Stat_t
var err error
err = syscall.Fstat(int(fd), &st)
if err != nil { return false }
if (st.Mode & syscall.S_IFMT) == syscall.S_IFCHR {
var term string
// i assume this fd is bound to the current terminal if it's a character device
// if the assumption is wrong, you simply get extraneous ansi code in the output.
term = os.Getenv("TERM")
if term != "" && term != "dumb" { return true }
}
return false
}
func NewAppLogger(id string, w io.Writer, mask hodu.LogMask) *AppLogger {
var l *AppLogger var l *AppLogger
var f *os.File
var ok bool
var use_color bool
use_color = false
f, ok = w.(*os.File)
if ok { use_color = _is_ansi_tty(f.Fd()) }
l = &AppLogger{ l = &AppLogger{
id: id, id: id,
out: w, out: w,
mask: mask, mask: mask,
msg_chan: make(chan app_logger_msg_t, 256), msg_chan: make(chan app_logger_msg_t, 256),
use_color: use_color,
} }
l.closed.Store(false) l.closed.Store(false)
l.wg.Add(1) l.wg.Add(1)
@ -45,7 +74,7 @@ func NewAppLogger (id string, w io.Writer, mask hodu.LogMask) *AppLogger {
return l return l
} }
func NewAppLoggerToFile (id string, file_name string, max_size int64, rotate int, mask hodu.LogMask) (*AppLogger, error) { func NewAppLoggerToFile(id string, file_name string, max_size int64, rotate int, mask hodu.LogMask) (*AppLogger, error) {
var l *AppLogger var l *AppLogger
var f *os.File var f *os.File
var matched bool var matched bool
@ -73,6 +102,7 @@ func NewAppLoggerToFile (id string, file_name string, max_size int64, rotate int
file_max_size: max_size, file_max_size: max_size,
file_rotate: rotate, file_rotate: rotate,
msg_chan: make(chan app_logger_msg_t, 256), msg_chan: make(chan app_logger_msg_t, 256),
use_color: _is_ansi_tty(f.Fd()),
} }
l.closed.Store(false) l.closed.Store(false)
l.wg.Add(1) l.wg.Add(1)
@ -121,7 +151,6 @@ main_loop:
} }
} }
func (l *AppLogger) Write(id string, level hodu.LogLevel, fmtstr string, args ...interface{}) { func (l *AppLogger) Write(id string, level hodu.LogLevel, fmtstr string, args ...interface{}) {
if l.mask & hodu.LogMask(level) == 0 { return } if l.mask & hodu.LogMask(level) == 0 { return }
l.write(id, level, 1, fmtstr, args...) l.write(id, level, 1, fmtstr, args...)
@ -148,10 +177,10 @@ func (l *AppLogger) write(id string, level hodu.LogLevel, call_depth int, fmtstr
now = time.Now() now = time.Now()
_, off_s = now.Zone() _, off_s = now.Zone()
off_m = off_s / 60; off_m = off_s / 60
off_h = off_m / 60; off_h = off_m / 60
off_m = off_m % 60; off_m = off_m % 60
if off_m < 0 { off_m = -off_m; } if off_m < 0 { off_m = -off_m }
sb.WriteString( sb.WriteString(
fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d %+03d%02d ", fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d %+03d%02d ",
@ -170,7 +199,15 @@ func (l *AppLogger) write(id string, level hodu.LogLevel, call_depth int, fmtstr
} }
sb.WriteString(": ") sb.WriteString(": ")
msg = fmt.Sprintf(fmtstr, args...) msg = fmt.Sprintf(fmtstr, args...)
if (l.use_color) {
var code string
code = l.log_level_to_ansi_code(level)
sb.WriteString(code)
sb.WriteString(msg) sb.WriteString(msg)
if code != "" { sb.WriteString("\x1B[0m") }
} else {
sb.WriteString(msg)
}
if msg[len(msg) - 1] != '\n' { sb.WriteRune('\n') } if msg[len(msg) - 1] != '\n' { sb.WriteRune('\n') }
// use queue to avoid blocking operation as much as possible // use queue to avoid blocking operation as much as possible
@ -212,3 +249,24 @@ func (l *AppLogger) rotate() {
l.out = l.file l.out = l.file
} }
} }
func (l* AppLogger) log_level_to_ansi_code(level hodu.LogLevel) string {
switch level {
case hodu.LOG_ERROR:
return "\x1B[31m" // red
case hodu.LOG_WARN:
return "\x1B[33m" // yellow
case hodu.LOG_INFO:
if (l.mask & hodu.LogMask(hodu.LOG_DEBUG)) != 0 {
// if debug is enabled, change the color of info.
// otherwisse no color
return "\x1B[32m" // green
}
fallthrough
default:
return ""
}
}

View File

@ -1,7 +1,6 @@
package main package main
import "context" import "context"
import "crypto/tls"
import _ "embed" import _ "embed"
import "flag" import "flag"
import "fmt" import "fmt"
@ -10,10 +9,11 @@ import "io"
import "net" import "net"
import "os" import "os"
import "os/signal" import "os/signal"
import "path/filepath"
import "regexp"
import "strings" import "strings"
import "sync" import "sync"
import "syscall" import "syscall"
import "time"
// Don't change these items to 'const' as they can be overridden externally with a linker option // Don't change these items to 'const' as they can be overridden externally with a linker option
var HODU_NAME string = "hodu" var HODU_NAME string = "hodu"
@ -23,6 +23,8 @@ var HODU_VERSION string = "0.0.0"
var hodu_tls_cert_text []byte var hodu_tls_cert_text []byte
//go:embed tls.key //go:embed tls.key
var hodu_tls_key_text []byte var hodu_tls_key_text []byte
//go:embed rsa.key
var hodu_rsa_key_text []byte
// -------------------------------------------------------------------- // --------------------------------------------------------------------
type signal_handler struct { type signal_handler struct {
@ -89,59 +91,82 @@ func (sh *signal_handler) WriteLog(id string, level hodu.LogLevel, fmt string, a
// -------------------------------------------------------------------- // --------------------------------------------------------------------
func server_main(ctl_addrs []string, rpc_addrs []string, pxy_addrs []string, wpx_addrs []string, cfg *ServerConfig) error { func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy_addrs []string, wpx_addrs []string, logfile string, cfg *ServerConfig) error {
var s *hodu.Server var s *hodu.Server
var ctltlscfg *tls.Config var config *hodu.ServerConfig
var rpctlscfg *tls.Config
var pxytlscfg *tls.Config
var wpxtlscfg *tls.Config
var ctl_prefix string
var logger *AppLogger var logger *AppLogger
var log_mask hodu.LogMask var logmask hodu.LogMask
var logfile string
var logfile_maxsize int64 var logfile_maxsize int64
var logfile_rotate int var logfile_rotate int
var max_rpc_conns int var pty_user string
var max_peers int var pty_shell string
var xterm_html_file string var xterm_html_file string
var xterm_html string var xterm_html string
var err error var err error
log_mask = hodu.LOG_ALL logmask = hodu.LOG_ALL
if cfg != nil { config = &hodu.ServerConfig{
ctltlscfg, err = make_tls_server_config(&cfg.CTL.TLS) CtlAddrs: ctl_addrs,
if err != nil { return err } RpcAddrs: rpc_addrs,
rpctlscfg, err = make_tls_server_config(&cfg.RPC.TLS) RpxAddrs: rpx_addrs,
if err != nil { return err } PxyAddrs: pxy_addrs,
pxytlscfg, err = make_tls_server_config(&cfg.PXY.TLS) WpxAddrs: wpx_addrs,
if err != nil { return err }
wpxtlscfg, err = make_tls_server_config(&cfg.WPX.TLS)
if err != nil { return err }
if len(ctl_addrs) <= 0 { ctl_addrs = cfg.CTL.Service.Addrs }
if len(rpc_addrs) <= 0 { rpc_addrs = cfg.RPC.Service.Addrs }
if len(pxy_addrs) <= 0 { pxy_addrs = cfg.PXY.Service.Addrs }
if len(wpx_addrs) <= 0 { wpx_addrs = cfg.WPX.Service.Addrs }
ctl_prefix = cfg.CTL.Service.Prefix
log_mask = log_strings_to_mask(cfg.APP.LogMask)
logfile = cfg.APP.LogFile
logfile_maxsize = cfg.APP.LogMaxSize
logfile_rotate = cfg.APP.LogRotate
max_rpc_conns = cfg.APP.MaxRpcConns
max_peers = cfg.APP.MaxPeers
xterm_html_file = cfg.APP.XtermHtmlFile
} }
if len(rpc_addrs) <= 0 { if cfg != nil {
config.CtlTls, err = make_tls_server_config(&cfg.CTL.TLS)
if err != nil { return err }
config.RpcTls, err = make_tls_server_config(&cfg.RPC.TLS)
if err != nil { return err }
config.RpxTls, err = make_tls_server_config(&cfg.RPX.TLS)
if err != nil { return err }
config.PxyTls, err = make_tls_server_config(&cfg.PXY.TLS)
if err != nil { return err }
config.PxyTargetTls, err = make_tls_client_config(&cfg.PXY.Target.TLS)
if err != nil { return err }
config.WpxTls, err = make_tls_server_config(&cfg.WPX.TLS)
if err != nil { return err }
if len(config.CtlAddrs) <= 0 { config.CtlAddrs = cfg.CTL.Service.Addrs }
if len(config.RpcAddrs) <= 0 { config.RpcAddrs = cfg.RPC.Service.Addrs }
if len(config.RpxAddrs) <= 0 { config.RpxAddrs = cfg.RPX.Service.Addrs }
if len(config.PxyAddrs) <= 0 { config.PxyAddrs = cfg.PXY.Service.Addrs }
if len(config.WpxAddrs) <= 0 { config.WpxAddrs = cfg.WPX.Service.Addrs }
config.RpxClientTokenAttrName = cfg.RPX.ClientToken.AttrName
if cfg.RPX.ClientToken.Regex != "" {
config.RpxClientTokenRegex, err = regexp.Compile(cfg.RPX.ClientToken.Regex)
if err != nil { return err }
}
config.RpxClientTokenSubmatchIndex = cfg.RPX.ClientToken.SubmatchIndex
config.CtlCors = cfg.CTL.Service.Cors
config.CtlAuth, err = make_http_auth_config(&cfg.CTL.Service.Auth)
if err != nil { return err }
config.CtlPrefix = cfg.CTL.Service.Prefix
config.RpcMaxConns = cfg.APP.MaxRpcConns
config.MaxPeers = cfg.APP.MaxPeers
pty_user = cfg.APP.PtyUser
pty_shell = cfg.APP.PtyShell
xterm_html_file = cfg.APP.XtermHtmlFile
logmask = log_strings_to_mask(cfg.APP.LogMask)
if logfile == "" { logfile = cfg.APP.LogFile }
logfile_maxsize = cfg.APP.LogMaxSize
logfile_rotate = cfg.APP.LogRotate
}
if len(config.RpcAddrs) <= 0 {
return fmt.Errorf("no rpc service addresses specified") return fmt.Errorf("no rpc service addresses specified")
} }
if logfile == "" { if logfile == "" {
logger = NewAppLogger("server", os.Stderr, log_mask) logger = NewAppLogger("server", os.Stderr, logmask)
} else { } else {
logger, err = NewAppLoggerToFile("server", logfile, logfile_maxsize, logfile_rotate, log_mask) logger, err = NewAppLoggerToFile("server", logfile, logfile_maxsize, logfile_rotate, logmask)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize logger - %s", err.Error()) return fmt.Errorf("failed to initialize logger - %s", err.Error())
} }
@ -156,28 +181,18 @@ func server_main(ctl_addrs []string, rpc_addrs []string, pxy_addrs []string, wpx
xterm_html = string(tmp) xterm_html = string(tmp)
} }
s, err = hodu.NewServer( s, err = hodu.NewServer(context.Background(), HODU_NAME, logger, config)
context.Background(),
logger,
ctl_addrs,
rpc_addrs,
pxy_addrs,
wpx_addrs,
ctl_prefix,
ctltlscfg,
rpctlscfg,
pxytlscfg,
wpxtlscfg,
max_rpc_conns,
max_peers)
if err != nil { if err != nil {
return fmt.Errorf("failed to create new server - %s", err.Error()) return fmt.Errorf("failed to create server - %s", err.Error())
} }
if pty_user != "" { s.SetPtyUser(pty_user) }
if pty_shell != "" { s.SetPtyShell(pty_shell) }
if xterm_html != "" { s.SetXtermHtml(xterm_html) } if xterm_html != "" { s.SetXtermHtml(xterm_html) }
s.StartService(nil) s.StartService(nil)
s.StartCtlService() s.StartCtlService()
s.StartRpxService()
s.StartPxyService() s.StartPxyService()
s.StartWpxService() s.StartWpxService()
s.StartExtService(&signal_handler{svc:s}, nil) s.StartExtService(&signal_handler{svc:s}, nil)
@ -248,50 +263,70 @@ func parse_client_route_config(v string) (*hodu.ClientRouteConfig, error) {
ptc_name = strings.TrimSpace(va[3]) ptc_name = strings.TrimSpace(va[3])
} }
return &hodu.ClientRouteConfig{PeerAddr: va[0], PeerName: ptc_name, Option: option, ServiceAddr: svc_addr}, nil // TODO: other fields return &hodu.ClientRouteConfig{PeerAddr: va[0], PeerName: ptc_name, ServiceOption: option, ServiceAddr: svc_addr}, nil // TODO: other fields
} }
func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string, cfg *ClientConfig) error { func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string, logfile string, cfg *ClientConfig) error {
var c *hodu.Client var c *hodu.Client
var ctltlscfg *tls.Config var config *hodu.ClientConfig
var rpctlscfg *tls.Config var cc hodu.ClientConnConfig
var ctl_prefix string
var cc hodu.ClientConfig
var logger *AppLogger var logger *AppLogger
var log_mask hodu.LogMask var logmask hodu.LogMask
var logfile string
var logfile_maxsize int64 var logfile_maxsize int64
var logfile_rotate int var logfile_rotate int
var max_rpc_conns int var pty_user string
var max_peers int var pty_shell string
var peer_conn_tmout time.Duration var xterm_html_file string
var xterm_html string
var i int var i int
var err error var err error
log_mask = hodu.LOG_ALL logmask = hodu.LOG_ALL
if cfg != nil {
ctltlscfg, err = make_tls_server_config(&cfg.CTL.TLS) config = &hodu.ClientConfig{
if err != nil { CtlAddrs: ctl_addrs,
return err
}
rpctlscfg, err = make_tls_client_config(&cfg.RPC.TLS)
if err != nil {
return err
} }
if len(ctl_addrs) <= 0 { ctl_addrs = cfg.CTL.Service.Addrs } if cfg != nil {
config.CtlTls, err = make_tls_server_config(&cfg.CTL.TLS)
if err != nil { return err }
config.RpcTls, err = make_tls_client_config(&cfg.RPC.TLS)
if err != nil { return err }
config.RpxTargetTls, err = make_tls_client_config(&cfg.RPX.Target.TLS)
if err != nil { return err }
if len(rpc_addrs) <= 0 { rpc_addrs = cfg.RPC.Endpoint.Addrs } if len(rpc_addrs) <= 0 { rpc_addrs = cfg.RPC.Endpoint.Addrs }
ctl_prefix = cfg.CTL.Service.Prefix if len(config.CtlAddrs) <= 0 { config.CtlAddrs = cfg.CTL.Service.Addrs }
config.RpxTargetAddr = cfg.RPX.Target.Addr
config.CtlPrefix = cfg.CTL.Service.Prefix
config.CtlCors = cfg.CTL.Service.Cors
config.CtlAuth, err = make_http_auth_config(&cfg.CTL.Service.Auth)
if err != nil { return err }
cc.ServerSeedTmout = cfg.RPC.Endpoint.SeedTmout cc.ServerSeedTmout = cfg.RPC.Endpoint.SeedTmout
cc.ServerAuthority = cfg.RPC.Endpoint.Authority cc.ServerAuthority = cfg.RPC.Endpoint.Authority
log_mask = log_strings_to_mask(cfg.APP.LogMask) logmask = log_strings_to_mask(cfg.APP.LogMask)
logfile = cfg.APP.LogFile if logfile == "" { logfile = cfg.APP.LogFile }
logfile_maxsize = cfg.APP.LogMaxSize logfile_maxsize = cfg.APP.LogMaxSize
logfile_rotate = cfg.APP.LogRotate logfile_rotate = cfg.APP.LogRotate
max_rpc_conns = cfg.APP.MaxRpcConns pty_user = cfg.APP.PtyUser
max_peers = cfg.APP.MaxPeers pty_shell = cfg.APP.PtyShell
peer_conn_tmout = cfg.APP.PeerConnTmout xterm_html_file = cfg.APP.XtermHtmlFile
config.RpcConnMax = cfg.APP.MaxRpcConns
config.PeerConnMax = cfg.APP.MaxPeers
config.PeerConnTmout = cfg.APP.PeerConnTmout
if cfg.APP.TokenText != "" {
config.Token = cfg.APP.TokenText
} else if cfg.APP.TokenFile != "" {
var bytes []byte
bytes, err = os.ReadFile(cfg.APP.TokenFile)
if err != nil {
return fmt.Errorf("unable to read token file - %s", err.Error())
}
config.Token = string(bytes)
}
} }
// unlke the server, we allow the client to start with no rpc address. // unlke the server, we allow the client to start with no rpc address.
@ -306,23 +341,28 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
} }
if logfile == "" { if logfile == "" {
logger = NewAppLogger("server", os.Stderr, log_mask) logger = NewAppLogger("client", os.Stderr, logmask)
} else { } else {
logger, err = NewAppLoggerToFile("server", logfile, logfile_maxsize, logfile_rotate, log_mask) logger, err = NewAppLoggerToFile("client", logfile, logfile_maxsize, logfile_rotate, logmask)
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize logger - %s", err.Error()) return fmt.Errorf("failed to initialize logger - %s", err.Error())
} }
} }
c = hodu.NewClient(
context.Background(), if xterm_html_file != "" {
logger, var tmp []byte
ctl_addrs, tmp, err = os.ReadFile(xterm_html_file)
ctl_prefix, if err != nil {
ctltlscfg, return fmt.Errorf("failed to read %s - %s", xterm_html_file, err.Error())
rpctlscfg, }
max_rpc_conns, xterm_html = string(tmp)
max_peers, }
peer_conn_tmout)
c = hodu.NewClient(context.Background(), HODU_NAME, logger, config)
if pty_user != "" { c.SetPtyUser(pty_user) }
if pty_shell != "" { c.SetPtyShell(pty_shell) }
if xterm_html != "" { c.SetXtermHtml(xterm_html) }
c.StartService(&cc) c.StartService(&cc)
c.StartCtlService() // control channel c.StartCtlService() // control channel
@ -342,14 +382,17 @@ func main() {
if strings.EqualFold(os.Args[1], "server") { if strings.EqualFold(os.Args[1], "server") {
var rpc_addrs []string var rpc_addrs []string
var ctl_addrs []string var ctl_addrs []string
var rpx_addrs []string
var pxy_addrs []string var pxy_addrs []string
var wpx_addrs []string var wpx_addrs []string
var cfgfile string var cfgfile string
var cfgpat string
var logfile string var logfile string
var cfg *ServerConfig var cfg ServerConfig
ctl_addrs = make([]string, 0) ctl_addrs = make([]string, 0)
rpc_addrs = make([]string, 0) rpc_addrs = make([]string, 0)
rpx_addrs = make([]string, 0)
pxy_addrs = make([]string, 0) pxy_addrs = make([]string, 0)
wpx_addrs = make([]string, 0) wpx_addrs = make([]string, 0)
@ -362,6 +405,10 @@ func main() {
rpc_addrs = append(rpc_addrs, v) rpc_addrs = append(rpc_addrs, v)
return nil return nil
}) })
flgs.Func("rpx-on", "specify a rpx listening address", func(v string) error {
rpx_addrs = append(rpx_addrs, v)
return nil
})
flgs.Func("pxy-on", "specify a proxy listening address", func(v string) error { flgs.Func("pxy-on", "specify a proxy listening address", func(v string) error {
pxy_addrs = append(pxy_addrs, v) pxy_addrs = append(pxy_addrs, v)
return nil return nil
@ -374,30 +421,50 @@ func main() {
logfile = v logfile = v
return nil return nil
}) })
flgs.Func("config-file", "specify a configuration file path", func(v string) error { flgs.Func("config-file", "specify a primary configuration file path", func(v string) error {
cfgfile = v cfgfile = v
return nil return nil
}) })
// TODO: add a command line option to specify log file and mask. flgs.Func("config-file-pattern", "specify a file pattern for additional configuration files", func(v string) error {
cfgpat = v
return nil
})
flgs.SetOutput(io.Discard) // prevent usage output flgs.SetOutput(io.Discard) // prevent usage output
err = flgs.Parse(os.Args[2:]) err = flgs.Parse(os.Args[2:])
if err != nil { if err != nil {
fmt.Printf ("ERROR: %s\n", err.Error()) fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
goto wrong_usage goto wrong_usage
} }
if flgs.NArg() > 0 { goto wrong_usage } if flgs.NArg() > 0 { goto wrong_usage }
if cfgfile != "" { if cfgfile != "" {
cfg, err = load_server_config(cfgfile) err = load_server_config_to(cfgfile, &cfg)
if err != nil { if err != nil {
fmt.Printf ("ERROR: failed to load configuration file %s - %s\n", cfgfile, err.Error()) fmt.Fprintf(os.Stderr, "ERROR: failed to load configuration file %s - %s\n", cfgfile, err.Error())
goto oops goto oops
} }
} }
if logfile != "" { cfg.APP.LogFile = logfile } if cfgpat != "" {
var file string
var matches []string
err = server_main(ctl_addrs, rpc_addrs, pxy_addrs, wpx_addrs, cfg) matches, err = filepath.Glob(cfgpat)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to match the pattern %s - %s\n", cfgpat, err.Error())
goto oops
}
for _, file = range matches {
err = load_server_config_to(file, &cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to load configuration file %s - %s\n", file, err.Error())
goto oops
}
}
}
err = server_main(ctl_addrs, rpc_addrs, rpx_addrs, pxy_addrs, wpx_addrs, logfile, &cfg)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: server error - %s\n", err.Error()) fmt.Fprintf(os.Stderr, "ERROR: server error - %s\n", err.Error())
goto oops goto oops
@ -406,8 +473,9 @@ func main() {
var rpc_addrs []string var rpc_addrs []string
var ctl_addrs []string var ctl_addrs []string
var cfgfile string var cfgfile string
var cfgpat string
var logfile string var logfile string
var cfg *ClientConfig var cfg ClientConfig
ctl_addrs = make([]string, 0) ctl_addrs = make([]string, 0)
rpc_addrs = make([]string, 0) rpc_addrs = make([]string, 0)
@ -429,24 +497,44 @@ func main() {
cfgfile = v cfgfile = v
return nil return nil
}) })
// TODO: add a command line option to specify log file and mask. flgs.Func("config-file-pattern", "specify a file pattern for additional configuration files", func(v string) error {
cfgpat = v
return nil
})
flgs.SetOutput(io.Discard) flgs.SetOutput(io.Discard)
err = flgs.Parse(os.Args[2:]) err = flgs.Parse(os.Args[2:])
if err != nil { if err != nil {
fmt.Printf ("ERROR: %s\n", err.Error()) fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
goto wrong_usage goto wrong_usage
} }
if cfgfile != "" { if cfgfile != "" {
cfg, err = load_client_config(cfgfile) err = load_client_config_to(cfgfile, &cfg)
if err != nil { if err != nil {
fmt.Printf ("ERROR: failed to load configuration file %s - %s\n", cfgfile, err.Error()) fmt.Fprintf(os.Stderr, "ERROR: failed to load configuration file %s - %s\n", cfgfile, err.Error())
goto oops goto oops
} }
} }
if logfile != "" { cfg.APP.LogFile = logfile } if cfgpat != "" {
var file string
var matches []string
err = client_main(ctl_addrs, rpc_addrs, flgs.Args(), cfg) matches, err = filepath.Glob(cfgpat)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to match the pattern %s - %s\n", cfgpat, err.Error())
goto oops
}
for _, file = range matches {
err = load_client_config_to(file, &cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to load configuration file %s - %s\n", file, err.Error())
goto oops
}
}
}
err = client_main(ctl_addrs, rpc_addrs, flgs.Args(), logfile, &cfg)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: client error - %s\n", err.Error()) fmt.Fprintf(os.Stderr, "ERROR: client error - %s\n", err.Error())
goto oops goto oops
@ -461,8 +549,8 @@ func main() {
os.Exit(0) os.Exit(0)
wrong_usage: wrong_usage:
fmt.Fprintf(os.Stderr, "USAGE: %s server --rpc-on=addr:port --ctl-on=addr:port --pxy-on=addr:port --wpx-on=addr:port [--config-file=file]\n", os.Args[0]) fmt.Fprintf(os.Stderr, "USAGE: %s server --rpc-on=addr:port --ctl-on=addr:port --rpx-on=addr:port --pxy-on=addr:port --wpx-on=addr:port [--config-file=file] [--config-file-pattern=pattern]\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s client --rpc-to=addr:port --ctl-on=addr:port [--config-file=file] [peer-addr:peer-port ...]\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s client --rpc-to=addr:port --ctl-on=addr:port [--config-file=file] [--config-file-pattern=pattern] [peer-addr:peer-port ...]\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s version\n", os.Args[0]) fmt.Fprintf(os.Stderr, " %s version\n", os.Args[0])
os.Exit(1) os.Exit(1)

27
cmd/rsa.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsTC9roInjDzu12tjv1CsOM4jvuB6/5vv+cmOMF5GLMVTnJCW
6U9onsOi6iN2rzlf5glkjdtijXCPL6QEX3YLYPD4NFCiOGIPhCHjWC4nBjI7LEEm
0SqrArMhPiyYLmnkA961a7mDw9dcr5JQBDq2ZyTe917N229Jr4PCZbHLboOxNlp3
QLSyxE5tfKZea53qm8SUF8maBvnOH8igvuYOek3iRMg3T+GoxCqy2gE1qznvwsaK
PdmTTzbIbc7XNU7t5yT6fZTvjUqs4WBuHqud4unE//KAT5vfxDdQFGcb45oMwxcK
bf03w4ZsBNvAcgCkWW+ophEOZRPkKrluHjVdNwIDAQABAoIBAARZ/5aNEL6TcoQs
2X7F0uz0NxGFfs/POxYF2q2aaxvHXtXOAT7KmfWoNVSNuWj1PkMugN8w/5scpA+V
9huIESB42oeiYVGEKwBiOqycOY4f5q8gDH1/kEKZNpxJyRT+ucBUlF0IadGB9P9E
1x07eeZPlAA8Pk8AzSz3zerkcmwM2lYYG851QyuiiTReSec3LLDcJvG5xAXZrIY0
Zwm7qv8uvjJGqMVYlywMnRngeNywP9ZaOJ38vdmWMu4bBF+QwydOAB9A7O9zluDZ
wK7OBedAZkRT15luZ1lkuTrKVZEaugD8dbt6BBLuhbPRRGuFb4WoNaVI3CRu9RSX
72gYkRECgYEA9x0IAFGc8DmCHOP/S+uy0VjvLGYh4QN3/0UOLRvoREzF0FtAxqci
bPASGmSCJEDL93JNjlxhITDUUawyOGRgAAXyAkE9MWmv18+pfTNTDeoaeXsBqcLz
f9LCNc3mCx93tvCK7gfIYs8Ef0QKfdrsQwMGlutgXmjE+pexNXPFWEcCgYEAt4/8
gsXi7tsCQp1YiP7VFZjoXSLejq+7pQrGV58PzlZKiOH/M5S6YS8wgm5oIEMLq2UP
nUn+FBCJ/I2b6HIdVq/Jr77XHcBFSZZEQbXe2gxTTucj6BTja1kSEilOquaaPvbR
WEs0+50rsgH0nLqSbMZZRkxOAUu9nObFvHA6O5ECgYEAzzd8+id13suam/dkoZlo
PbzB8w1B45oxCdIybQk13/AxAONEklCcwZUe2RrnNtdPMpSbDIHSwS5dHI+1HSyu
g9Z4dgOW+NSTK/lrOx3Ky6Q/xxaq8lwULF/jk5KxESq2DKXxGmFUW+cU8lNwKNFn
xVnIMM335bMdWrXRV+1Y0wkCgYBbXYOl47Esij35wi+LIKwW7+DYWr7D7pxLba2D
d1x6q2C1+Sb5GZIbRU2z3hhd1oE8cjTvaSDaA9Fqr2FmtUX9G8obe7W+zTCvi+e1
fTzK80+T+mBY5+y6Rb9E4uKRFe64YEma1PQuOPDCzU5fpE21bpSI9PnukzBxpDvP
q1yQwQKBgQCXiW8UghuwIp3INFzBTedBHNKBwRd82ZIhBWLcgWxC/EyWsRRFpJj4
HlVRYOvi2Q3DV6+Yn8zg3OeBhudGfCRCTkENbzAalcWqr9qb3Q4y26tZZQ9yNKk1
jJ2OfVw4K/6L49iVNF/2kLdbRebQXwngQUmiZSai5MlrHOFYkkiwaA==
-----END RSA PRIVATE KEY-----

15
go.mod
View File

@ -3,13 +3,24 @@ module hodu
go 1.22.0 go 1.22.0
require ( require (
github.com/creack/pty v1.1.24
github.com/goccy/go-yaml v1.17.1
github.com/prometheus/client_golang v1.20.5
golang.org/x/crypto v0.26.0 golang.org/x/crypto v0.26.0
golang.org/x/net v0.28.0 golang.org/x/net v0.28.0
golang.org/x/sys v0.24.0 golang.org/x/sys v0.24.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.34.2 google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1
) )
require google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
)

26
go.sum
View File

@ -1,5 +1,27 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
@ -16,7 +38,3 @@ google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

362
hodu.go
View File

@ -1,15 +1,20 @@
package hodu package hodu
import "bytes"
import "crypto/rsa"
import _ "embed"
import "encoding/base64"
import "fmt"
import "net" import "net"
import "net/http" import "net/http"
import "net/netip" import "net/netip"
import "os" import "os"
import "regexp"
import "runtime" import "runtime"
import "strings" import "strings"
import "sync" import "sync"
import "time" import "time"
const HODU_RPC_VERSION uint32 = 0x010000 const HODU_RPC_VERSION uint32 = 0x010000
type LogLevel int type LogLevel int
@ -28,6 +33,10 @@ const LOG_NONE LogMask = LogMask(0)
var IPV4_PREFIX_ZERO = netip.MustParsePrefix("0.0.0.0/0") var IPV4_PREFIX_ZERO = netip.MustParsePrefix("0.0.0.0/0")
var IPV6_PREFIX_ZERO = netip.MustParsePrefix("::/0") var IPV6_PREFIX_ZERO = netip.MustParsePrefix("::/0")
type Named struct {
name string
}
type Logger interface { type Logger interface {
Write(id string, level LogLevel, fmtstr string, args ...interface{}) Write(id string, level LogLevel, fmtstr string, args ...interface{})
WriteWithCallDepth(id string, level LogLevel, call_depth int, fmtstr string, args ...interface{}) WriteWithCallDepth(id string, level LogLevel, call_depth int, fmtstr string, args ...interface{})
@ -44,6 +53,100 @@ type Service interface {
WriteLog(id string, level LogLevel, fmtstr string, args ...interface{}) WriteLog(id string, level LogLevel, fmtstr string, args ...interface{})
} }
type HttpAccessAction int
const (
HTTP_ACCESS_ACCEPT HttpAccessAction = iota
HTTP_ACCESS_REJECT
HTTP_ACCESS_AUTH_REQUIRED
)
type HttpAccessRule struct {
Prefix string
OrgNets []netip.Prefix
Action HttpAccessAction
}
type HttpAuthCredMap map[string]string
type HttpAuthConfig struct {
Enabled bool
Realm string
Creds HttpAuthCredMap
TokenTtl time.Duration
TokenRsaKey *rsa.PrivateKey
AccessRules []HttpAccessRule
}
type JsonErrmsg struct {
Text string `json:"error-text"`
}
type json_in_notice struct {
Text string `json:"text"`
}
type json_out_go_stats struct {
CPUs int `json:"cpus"`
Goroutines int `json:"goroutines"`
NumCgoCalls int64 `json:"num-cgo-calls"`
NumGCs uint32 `json:"num-gcs"`
AllocBytes uint64 `json:"memory-alloc-bytes"`
TotalAllocBytes uint64 `json:"memory-total-alloc-bytes"`
SysBytes uint64 `json:"memory-sys-bytes"`
Lookups uint64 `json:"memory-lookups"`
MemAllocs uint64 `json:"memory-num-allocs"`
MemFrees uint64 `json:"memory-num-frees"`
HeapAllocBytes uint64 `json:"memory-heap-alloc-bytes"`
HeapSysBytes uint64 `json:"memory-heap-sys-bytes"`
HeapIdleBytes uint64 `json:"memory-heap-idle-bytes"`
HeapInuseBytes uint64 `json:"memory-heap-inuse-bytes"`
HeapReleasedBytes uint64 `json:"memory-heap-released-bytes"`
HeapObjects uint64 `json:"memory-heap-objects"`
StackInuseBytes uint64 `json:"memory-stack-inuse-bytes"`
StackSysBytes uint64 `json:"memory-stack-sys-bytes"`
MSpanInuseBytes uint64 `json:"memory-mspan-inuse-bytes"`
MSpanSysBytes uint64 `json:"memory-mspan-sys-bytes"`
MCacheInuseBytes uint64 `json:"memory-mcache-inuse-bytes"`
MCacheSysBytes uint64 `json:"memory-mcache-sys-bytes"`
BuckHashSysBytes uint64 `json:"memory-buck-hash-sys-bytes"`
GCSysBytes uint64 `json:"memory-gc-sys-bytes"`
OtherSysBytes uint64 `json:"memory-other-sys-bytes"`
}
type json_xterm_ws_event struct {
Type string `json:"type"`
Data []string `json:"data"`
}
// ---------------------------------------------------------
//go:embed xterm.js
var xterm_js []byte
//go:embed xterm-addon-fit.js
var xterm_addon_fit_js []byte
//go:embed xterm.css
var xterm_css []byte
//go:embed xterm.html
var xterm_html string
type xterm_session_info struct {
Mode string
ConnId string
RouteId string
}
// ---------------------------------------------------------
func (n *Named) SetName(name string) {
n.name = name
}
func (n *Named) Name() string {
return n.name
}
func TcpAddrStrClass(addr string) string { func TcpAddrStrClass(addr string) string {
// the string is supposed to be addr:port // the string is supposed to be addr:port
@ -53,7 +156,7 @@ func TcpAddrStrClass(addr string) string {
ap, err = netip.ParseAddrPort(addr) ap, err = netip.ParseAddrPort(addr)
if err == nil { if err == nil {
if ap.Addr().Is6() { return "tcp6" } if ap.Addr().Is6() { return "tcp6" }
if ap.Addr().Is4() { return "tcp4" } if ap.Addr().Is4() || ap.Addr().Is4In6() { return "tcp4" }
} }
} }
@ -61,7 +164,9 @@ func TcpAddrStrClass(addr string) string {
} }
func TcpAddrClass(addr *net.TCPAddr) string { func TcpAddrClass(addr *net.TCPAddr) string {
if addr.AddrPort().Addr().Is4() { var netip_addr netip.Addr
netip_addr = addr.AddrPort().Addr()
if netip_addr.Is4() || netip_addr.Is4In6() {
return "tcp4" return "tcp4"
} else { } else {
return "tcp6" return "tcp6"
@ -76,8 +181,6 @@ func word_to_route_option(word string) RouteOption {
return RouteOption(ROUTE_OPTION_TCP6) return RouteOption(ROUTE_OPTION_TCP6)
case "tcp": case "tcp":
return RouteOption(ROUTE_OPTION_TCP) return RouteOption(ROUTE_OPTION_TCP)
case "tty":
return RouteOption(ROUTE_OPTION_TTY)
case "http": case "http":
return RouteOption(ROUTE_OPTION_HTTP) return RouteOption(ROUTE_OPTION_HTTP)
case "https": case "https":
@ -89,7 +192,7 @@ func word_to_route_option(word string) RouteOption {
return RouteOption(ROUTE_OPTION_UNSPEC) return RouteOption(ROUTE_OPTION_UNSPEC)
} }
func string_to_route_option(desc string) RouteOption { func StringToRouteOption(desc string) RouteOption {
var fld string var fld string
var option RouteOption var option RouteOption
var p RouteOption var p RouteOption
@ -103,13 +206,12 @@ func string_to_route_option(desc string) RouteOption {
return option return option
} }
func (option RouteOption) string() string { func (option RouteOption) String() string {
var str string var str string
str = "" str = ""
if option & RouteOption(ROUTE_OPTION_TCP6) != 0 { str += " tcp6" } if option & RouteOption(ROUTE_OPTION_TCP6) != 0 { str += " tcp6" }
if option & RouteOption(ROUTE_OPTION_TCP4) != 0 { str += " tcp4" } if option & RouteOption(ROUTE_OPTION_TCP4) != 0 { str += " tcp4" }
if option & RouteOption(ROUTE_OPTION_TCP) != 0 { str += " tcp" } if option & RouteOption(ROUTE_OPTION_TCP) != 0 { str += " tcp" }
if option & RouteOption(ROUTE_OPTION_TTY) != 0 { str += " tty" }
if option & RouteOption(ROUTE_OPTION_HTTP) != 0 { str += " http" } if option & RouteOption(ROUTE_OPTION_HTTP) != 0 { str += " http" }
if option & RouteOption(ROUTE_OPTION_HTTPS) != 0 { str += " https" } if option & RouteOption(ROUTE_OPTION_HTTPS) != 0 { str += " https" }
if option & RouteOption(ROUTE_OPTION_SSH) != 0 { str += " ssh" } if option & RouteOption(ROUTE_OPTION_SSH) != 0 { str += " ssh" }
@ -119,8 +221,9 @@ func (option RouteOption) string() string {
func dump_call_frame_and_exit(log Logger, req *http.Request, err interface{}) { func dump_call_frame_and_exit(log Logger, req *http.Request, err interface{}) {
var buf []byte var buf []byte
buf = make([]byte, 65536); buf = buf[:min(65536, runtime.Stack(buf, false))] buf = make([]byte, 65536)
log.Write("", LOG_ERROR, "[%s] %s %s - %v\n%s", req.RemoteAddr, req.Method, req.URL.String(), err, string(buf)) buf = buf[:min(65536, runtime.Stack(buf, false))]
log.Write("", LOG_ERROR, "[%s] %s %s - %v\n%s", req.RemoteAddr, req.Method, req.RequestURI, err, string(buf))
log.Close() log.Close()
os.Exit(99) // fatal error. treat panic() as a fatal runtime error os.Exit(99) // fatal error. treat panic() as a fatal runtime error
} }
@ -153,7 +256,7 @@ func get_last_rune_of_non_empty_string(s string) rune {
return tmp[len(tmp) - 1] return tmp[len(tmp) - 1]
} }
func parse_duration_string(dur string) (time.Duration, error) { func ParseDurationString(dur string) (time.Duration, error) {
// i want the input to be in seconds with resolution of 9 digits after // i want the input to be in seconds with resolution of 9 digits after
// the decimal point. For example, 0.05 to mean 500ms. // the decimal point. For example, 0.05 to mean 500ms.
// however, i don't care if a unit is part of the input. // however, i don't care if a unit is part of the input.
@ -166,13 +269,19 @@ func parse_duration_string(dur string) (time.Duration, error) {
return time.ParseDuration(tmp) return time.ParseDuration(tmp)
} }
func DurationToSecString(d time.Duration) string {
return fmt.Sprintf("%.09f", d.Seconds())
}
// ------------------------------------
func WriteJsonRespHeader(w http.ResponseWriter, status_code int) int { func WriteJsonRespHeader(w http.ResponseWriter, status_code int) int {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status_code) w.WriteHeader(status_code)
return status_code return status_code
} }
func write_js_resp_header(w http.ResponseWriter, status_code int) int { func WriteJsRespHeader(w http.ResponseWriter, status_code int) int {
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
w.WriteHeader(status_code) w.WriteHeader(status_code)
return status_code return status_code
@ -195,6 +304,8 @@ func WriteEmptyRespHeader(w http.ResponseWriter, status_code int) int {
return status_code return status_code
} }
// ------------------------------------
func server_route_to_proxy_info(r *ServerRoute) *ServerRouteProxyInfo { func server_route_to_proxy_info(r *ServerRoute) *ServerRouteProxyInfo {
return &ServerRouteProxyInfo{ return &ServerRouteProxyInfo{
SvcOption: r.SvcOption, SvcOption: r.SvcOption,
@ -214,3 +325,230 @@ func proxy_info_to_server_route(pi *ServerRouteProxyInfo) *ServerRoute {
SvcPermNet: pi.SvcPermNet, SvcPermNet: pi.SvcPermNet,
} }
} }
// ------------------------------------
func (stats *json_out_go_stats) from_runtime_stats() {
var mstat runtime.MemStats
runtime.ReadMemStats(&mstat)
stats.CPUs = runtime.NumCPU()
stats.Goroutines = runtime.NumGoroutine()
stats.NumCgoCalls = runtime.NumCgoCall()
stats.NumGCs = mstat.NumGC
stats.AllocBytes = mstat.Alloc
stats.TotalAllocBytes = mstat.TotalAlloc
stats.SysBytes = mstat.Sys
stats.Lookups = mstat.Lookups
stats.MemAllocs = mstat.Mallocs
stats.MemFrees = mstat.Frees
stats.HeapAllocBytes = mstat.HeapAlloc
stats.HeapSysBytes = mstat.HeapSys
stats.HeapIdleBytes = mstat.HeapIdle
stats.HeapInuseBytes = mstat.HeapInuse
stats.HeapReleasedBytes = mstat.HeapReleased
stats.HeapObjects = mstat.HeapObjects
stats.StackInuseBytes = mstat.StackInuse
stats.StackSysBytes = mstat.StackSys
stats.MSpanInuseBytes = mstat.MSpanInuse
stats.MSpanSysBytes = mstat.MSpanSys
stats.MCacheInuseBytes = mstat.MCacheInuse
stats.MCacheSysBytes = mstat.MCacheSys
stats.BuckHashSysBytes = mstat.BuckHashSys
stats.GCSysBytes = mstat.GCSys
stats.OtherSysBytes = mstat.OtherSys
}
// ------------------------------------
func (auth *HttpAuthConfig) Authenticate(req *http.Request) (int, string) {
var rule HttpAccessRule
var raddrport netip.AddrPort
var raddr netip.Addr
var err error
raddrport, err = netip.ParseAddrPort(req.RemoteAddr)
if err == nil { raddr = raddrport.Addr() }
for _, rule = range auth.AccessRules {
// i don't take into account X-Forwarded-For and similar headers
var pfxd string = rule.Prefix
if pfxd[len(pfxd) -1] != '/' { pfxd = pfxd + "/" }
if req.URL.Path == rule.Prefix || strings.HasPrefix(req.URL.Path, pfxd) {
var org_net_ok bool
if len(rule.OrgNets) > 0 && raddr.IsValid() {
var netpfx netip.Prefix
org_net_ok = false
for _, netpfx = range rule.OrgNets {
if err == nil && netpfx.Contains(raddr) {
org_net_ok = true
break
}
}
} else {
org_net_ok = true
}
if org_net_ok {
if rule.Action == HTTP_ACCESS_ACCEPT {
return http.StatusOK, ""
} else if rule.Action == HTTP_ACCESS_REJECT {
return http.StatusForbidden, ""
}
// HTTP_ACCESS_AUTH_REQUIRED.
// move on to authentication if enabled. acceped if disabled
break
}
}
}
if auth != nil && auth.Enabled {
var auth_hdr string
var username string
var password string
var credpass string
var ok bool
auth_hdr = req.Header.Get("Authorization")
if auth_hdr != "" {
var auth_parts []string
auth_parts = strings.Fields(auth_hdr)
if len(auth_parts) == 2 && strings.EqualFold(auth_parts[0], "Bearer") && auth.TokenRsaKey != nil {
var jwt *JWT[ServerTokenClaim]
var claim ServerTokenClaim
jwt = NewJWT(auth.TokenRsaKey, &claim)
err = jwt.VerifyRS512(strings.TrimSpace(auth_parts[1]))
if err == nil {
// verification ok. let's check the actual payload
var now time.Time
now = time.Now()
if now.After(time.Unix(claim.IssuedAt, 0)) && now.Before(time.Unix(claim.ExpiresAt, 0)) { return http.StatusOK, "" } // not expired
}
}
}
// this application wants these two header values to be base64-encoded
username = req.Header.Get("X-Auth-Username")
password = req.Header.Get("X-Auth-Password")
if username != "" {
var tmp []byte
tmp, err = base64.StdEncoding.DecodeString(username)
if err != nil { return http.StatusBadRequest, "" }
username = string(tmp)
}
if password != "" {
var tmp []byte
tmp, err = base64.StdEncoding.DecodeString(password)
if err != nil { return http.StatusBadRequest, "" }
password = string(tmp)
}
// fall back to basic authentication
if username == "" && password == "" && auth.Realm != "" {
username, password, ok = req.BasicAuth()
if !ok { return http.StatusUnauthorized, auth.Realm }
}
credpass, ok = auth.Creds[username]
if !ok || credpass != password {
return http.StatusUnauthorized, auth.Realm
}
}
return http.StatusOK, ""
}
// ------------------------------------
func get_http_req_line_and_headers(r *http.Request, force_host bool) []byte {
var buf bytes.Buffer
var name string
var value string
var values []string
var host_found bool
var x_forwarded_host_found bool
var x_forwarded_proto_found bool
fmt.Fprintf(&buf, "%s %s %s\r\n", r.Method, r.RequestURI, r.Proto)
for name, values = range r.Header {
if strings.EqualFold(name, "Accept-Encoding") { // TODO: make it generic. parameterize it??
// skip Accept-Encoding as the go client side
// doesn't function properly when a certain enconding
// is specified. resp.Body.Read() returned EOF when
// not working
continue
} else if strings.EqualFold(name, "Host") {
host_found = true
} else if strings.EqualFold(name, "X-Forwarded-Host") {
x_forwarded_host_found = true
} else if strings.EqualFold(name, "X-Forwarded-Proto") {
x_forwarded_proto_found = true
}
for _, value = range values {
fmt.Fprintf(&buf, "%s: %s\r\n", name, value)
}
}
if force_host && !host_found && r.Host != "" {
fmt.Fprintf(&buf, "Host: %s\r\n", r.Host)
}
if !x_forwarded_host_found && r.Host != "" {
fmt.Fprintf(&buf, "X-Forwarded-Host: %s\r\n", r.Host)
}
if !x_forwarded_proto_found && r.Host != "" {
var proto string
if r.TLS != nil { proto = "https" } else { proto = "http" }
fmt.Fprintf(&buf, "X-Forwarded-Proto: %s\r\n", proto)
}
// TODO: host and x-forwarded-for, etc???
buf.WriteString("\r\n") // End of headers
return buf.Bytes()
}
func get_http_resp_line_and_headers(r *http.Response) []byte {
var buf bytes.Buffer
var name string
var value string
var values []string
fmt.Fprintf(&buf, "%s %s\r\n", r.Proto, r.Status)
for name, values = range r.Header {
for _, value = range values {
fmt.Fprintf(&buf, "%s: %s\r\n", name, value)
}
}
buf.WriteString("\r\n") // End of headers
return buf.Bytes()
}
func get_regex_submatch(re *regexp.Regexp, str string, n int) string {
var idxs []int
var pos int
var start int
var end int
idxs = re.FindStringSubmatchIndex(str)
if idxs == nil { return "" }
pos = n * 2
if pos + 1 >= len(idxs) { return "" }
start, end = idxs[pos], idxs[pos + 1]
if start == -1 || end == -1 {
return ""
}
return str[start:end]
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.35.1 // protoc-gen-go v1.36.7
// protoc v3.19.6 // protoc v3.19.6
// source: hodu.proto // source: hodu.proto
@ -11,6 +11,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe"
) )
const ( const (
@ -27,10 +28,9 @@ const (
ROUTE_OPTION_TCP ROUTE_OPTION = 1 ROUTE_OPTION_TCP ROUTE_OPTION = 1
ROUTE_OPTION_TCP4 ROUTE_OPTION = 2 ROUTE_OPTION_TCP4 ROUTE_OPTION = 2
ROUTE_OPTION_TCP6 ROUTE_OPTION = 4 ROUTE_OPTION_TCP6 ROUTE_OPTION = 4
ROUTE_OPTION_TTY ROUTE_OPTION = 8 ROUTE_OPTION_HTTP ROUTE_OPTION = 8
ROUTE_OPTION_HTTP ROUTE_OPTION = 16 ROUTE_OPTION_HTTPS ROUTE_OPTION = 16
ROUTE_OPTION_HTTPS ROUTE_OPTION = 32 ROUTE_OPTION_SSH ROUTE_OPTION = 32
ROUTE_OPTION_SSH ROUTE_OPTION = 64
) )
// Enum value maps for ROUTE_OPTION. // Enum value maps for ROUTE_OPTION.
@ -40,20 +40,18 @@ var (
1: "TCP", 1: "TCP",
2: "TCP4", 2: "TCP4",
4: "TCP6", 4: "TCP6",
8: "TTY", 8: "HTTP",
16: "HTTP", 16: "HTTPS",
32: "HTTPS", 32: "SSH",
64: "SSH",
} }
ROUTE_OPTION_value = map[string]int32{ ROUTE_OPTION_value = map[string]int32{
"UNSPEC": 0, "UNSPEC": 0,
"TCP": 1, "TCP": 1,
"TCP4": 2, "TCP4": 2,
"TCP6": 4, "TCP6": 4,
"TTY": 8, "HTTP": 8,
"HTTP": 16, "HTTPS": 16,
"HTTPS": 32, "SSH": 32,
"SSH": 64,
} }
) )
@ -97,6 +95,17 @@ const (
PACKET_KIND_PEER_ABORTED PACKET_KIND = 7 PACKET_KIND_PEER_ABORTED PACKET_KIND = 7
PACKET_KIND_PEER_EOF PACKET_KIND = 8 PACKET_KIND_PEER_EOF PACKET_KIND = 8
PACKET_KIND_PEER_DATA PACKET_KIND = 9 PACKET_KIND_PEER_DATA PACKET_KIND = 9
PACKET_KIND_CONN_DESC PACKET_KIND = 11
PACKET_KIND_CONN_ERROR PACKET_KIND = 12
PACKET_KIND_CONN_NOTICE PACKET_KIND = 13
PACKET_KIND_RPTY_START PACKET_KIND = 14
PACKET_KIND_RPTY_STOP PACKET_KIND = 15
PACKET_KIND_RPTY_DATA PACKET_KIND = 16
PACKET_KIND_RPTY_SIZE PACKET_KIND = 17 // terminal size
PACKET_KIND_RPX_START PACKET_KIND = 18
PACKET_KIND_RPX_STOP PACKET_KIND = 19
PACKET_KIND_RPX_DATA PACKET_KIND = 20
PACKET_KIND_RPX_EOF PACKET_KIND = 21
) )
// Enum value maps for PACKET_KIND. // Enum value maps for PACKET_KIND.
@ -112,6 +121,17 @@ var (
7: "PEER_ABORTED", 7: "PEER_ABORTED",
8: "PEER_EOF", 8: "PEER_EOF",
9: "PEER_DATA", 9: "PEER_DATA",
11: "CONN_DESC",
12: "CONN_ERROR",
13: "CONN_NOTICE",
14: "RPTY_START",
15: "RPTY_STOP",
16: "RPTY_DATA",
17: "RPTY_SIZE",
18: "RPX_START",
19: "RPX_STOP",
20: "RPX_DATA",
21: "RPX_EOF",
} }
PACKET_KIND_value = map[string]int32{ PACKET_KIND_value = map[string]int32{
"RESERVED": 0, "RESERVED": 0,
@ -124,6 +144,17 @@ var (
"PEER_ABORTED": 7, "PEER_ABORTED": 7,
"PEER_EOF": 8, "PEER_EOF": 8,
"PEER_DATA": 9, "PEER_DATA": 9,
"CONN_DESC": 11,
"CONN_ERROR": 12,
"CONN_NOTICE": 13,
"RPTY_START": 14,
"RPTY_STOP": 15,
"RPTY_DATA": 16,
"RPTY_SIZE": 17,
"RPX_START": 18,
"RPX_STOP": 19,
"RPX_DATA": 20,
"RPX_EOF": 21,
} }
) )
@ -155,12 +186,11 @@ func (PACKET_KIND) EnumDescriptor() ([]byte, []int) {
} }
type Seed struct { type Seed struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Version uint32 `protobuf:"varint,1,opt,name=Version,proto3" json:"Version,omitempty"` Version uint32 `protobuf:"varint,1,opt,name=Version,proto3" json:"Version,omitempty"`
Flags uint64 `protobuf:"varint,2,opt,name=Flags,proto3" json:"Flags,omitempty"` Flags uint64 `protobuf:"varint,2,opt,name=Flags,proto3" json:"Flags,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *Seed) Reset() { func (x *Seed) Reset() {
@ -208,30 +238,29 @@ func (x *Seed) GetFlags() uint64 {
} }
type RouteDesc struct { type RouteDesc struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"` RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"`
// C->S(ROUTE_START): client-side peer address // C->S(ROUTE_START/STOP): client-side peer address
// S->C(ROUTE_STARTED): server-side listening address // S->C(ROUTE_STARTED/STOPPED): server-side listening address
TargetAddrStr string `protobuf:"bytes,2,opt,name=TargetAddrStr,proto3" json:"TargetAddrStr,omitempty"` TargetAddrStr string `protobuf:"bytes,2,opt,name=TargetAddrStr,proto3" json:"TargetAddrStr,omitempty"`
// C->S(ROUTE_START): human-readable name of client-side peer // C->S(ROUTE_START/STOPPED): human-readable name of client-side peer
// S->C(ROUTE_STARTED): clone as sent by C // S->C(ROUTE_STARTED/STOPPED): clone as sent by C
TargetName string `protobuf:"bytes,3,opt,name=TargetName,proto3" json:"TargetName,omitempty"` TargetName string `protobuf:"bytes,3,opt,name=TargetName,proto3" json:"TargetName,omitempty"`
// C->S(ROUTE_START): desired listening option on the server-side(e.g. tcp, tcp4, tcp6) + // C->S(ROUTE_START): requested listening option on the server-side(e.g. tcp, tcp4, tcp6) +
// //
// hint to the service-side peer(e.g. local) + // hint to the service-side peer(e.g. local) +
// hint to the client-side peer(e.g. tty, http, https) // hint to the client-side peer(e.g. tty, http, https)
// //
// S->C(ROUTE_STARTED): cloned as sent by C. // S->C(ROUTE_STARTED): cloned as sent by C.
ServiceOption uint32 `protobuf:"varint,4,opt,name=ServiceOption,proto3" json:"ServiceOption,omitempty"` ServiceOption uint32 `protobuf:"varint,4,opt,name=ServiceOption,proto3" json:"ServiceOption,omitempty"`
// C->S(ROUTE_START): desired lisening address on the service-side // C->S(ROUTE_START): requested lisening address on the service-side
// S->C(ROUTE_STARTED): cloned as sent by C // S->C(ROUTE_STARTED): cloned as sent by C
ServiceAddrStr string `protobuf:"bytes,5,opt,name=ServiceAddrStr,proto3" json:"ServiceAddrStr,omitempty"` ServiceAddrStr string `protobuf:"bytes,5,opt,name=ServiceAddrStr,proto3" json:"ServiceAddrStr,omitempty"`
// C->S(ROUTE_START): permitted network of server-side peers. // C->S(ROUTE_START): requested permitted network of server-side peers.
// S->C(ROUTE_STARTED): cloned as sent by C. // S->C(ROUTE_STARTED): actual permitted network of server-side peers
ServiceNetStr string `protobuf:"bytes,6,opt,name=ServiceNetStr,proto3" json:"ServiceNetStr,omitempty"` ServiceNetStr string `protobuf:"bytes,6,opt,name=ServiceNetStr,proto3" json:"ServiceNetStr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *RouteDesc) Reset() { func (x *RouteDesc) Reset() {
@ -307,14 +336,13 @@ func (x *RouteDesc) GetServiceNetStr() string {
} }
type PeerDesc struct { type PeerDesc struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"` RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"`
PeerId uint32 `protobuf:"varint,2,opt,name=PeerId,proto3" json:"PeerId,omitempty"` PeerId uint32 `protobuf:"varint,2,opt,name=PeerId,proto3" json:"PeerId,omitempty"`
RemoteAddrStr string `protobuf:"bytes,3,opt,name=RemoteAddrStr,proto3" json:"RemoteAddrStr,omitempty"` RemoteAddrStr string `protobuf:"bytes,3,opt,name=RemoteAddrStr,proto3" json:"RemoteAddrStr,omitempty"`
LocalAddrStr string `protobuf:"bytes,4,opt,name=LocalAddrStr,proto3" json:"LocalAddrStr,omitempty"` LocalAddrStr string `protobuf:"bytes,4,opt,name=LocalAddrStr,proto3" json:"LocalAddrStr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *PeerDesc) Reset() { func (x *PeerDesc) Reset() {
@ -376,13 +404,12 @@ func (x *PeerDesc) GetLocalAddrStr() string {
} }
type PeerData struct { type PeerData struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"` RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"`
PeerId uint32 `protobuf:"varint,2,opt,name=PeerId,proto3" json:"PeerId,omitempty"` PeerId uint32 `protobuf:"varint,2,opt,name=PeerId,proto3" json:"PeerId,omitempty"`
Data []byte `protobuf:"bytes,3,opt,name=Data,proto3" json:"Data,omitempty"` Data []byte `protobuf:"bytes,3,opt,name=Data,proto3" json:"Data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *PeerData) Reset() { func (x *PeerData) Reset() {
@ -436,23 +463,271 @@ func (x *PeerData) GetData() []byte {
return nil return nil
} }
type Packet struct { type ConnDesc struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache Token string `protobuf:"bytes,1,opt,name=Token,proto3" json:"Token,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnDesc) Reset() {
*x = ConnDesc{}
mi := &file_hodu_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnDesc) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnDesc) ProtoMessage() {}
func (x *ConnDesc) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnDesc.ProtoReflect.Descriptor instead.
func (*ConnDesc) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{4}
}
func (x *ConnDesc) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
type ConnError struct {
state protoimpl.MessageState `protogen:"open.v1"`
ErrorId uint32 `protobuf:"varint,1,opt,name=ErrorId,proto3" json:"ErrorId,omitempty"`
Text string `protobuf:"bytes,2,opt,name=Text,proto3" json:"Text,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnError) Reset() {
*x = ConnError{}
mi := &file_hodu_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnError) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnError) ProtoMessage() {}
func (x *ConnError) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnError.ProtoReflect.Descriptor instead.
func (*ConnError) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{5}
}
func (x *ConnError) GetErrorId() uint32 {
if x != nil {
return x.ErrorId
}
return 0
}
func (x *ConnError) GetText() string {
if x != nil {
return x.Text
}
return ""
}
type ConnNotice struct {
state protoimpl.MessageState `protogen:"open.v1"`
Text string `protobuf:"bytes,1,opt,name=Text,proto3" json:"Text,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ConnNotice) Reset() {
*x = ConnNotice{}
mi := &file_hodu_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ConnNotice) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ConnNotice) ProtoMessage() {}
func (x *ConnNotice) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ConnNotice.ProtoReflect.Descriptor instead.
func (*ConnNotice) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{6}
}
func (x *ConnNotice) GetText() string {
if x != nil {
return x.Text
}
return ""
}
type RptyEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RptyEvent) Reset() {
*x = RptyEvent{}
mi := &file_hodu_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RptyEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RptyEvent) ProtoMessage() {}
func (x *RptyEvent) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RptyEvent.ProtoReflect.Descriptor instead.
func (*RptyEvent) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{7}
}
func (x *RptyEvent) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *RptyEvent) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
type RpxEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id uint64 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RpxEvent) Reset() {
*x = RpxEvent{}
mi := &file_hodu_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RpxEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RpxEvent) ProtoMessage() {}
func (x *RpxEvent) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RpxEvent.ProtoReflect.Descriptor instead.
func (*RpxEvent) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{8}
}
func (x *RpxEvent) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *RpxEvent) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
type Packet struct {
state protoimpl.MessageState `protogen:"open.v1"`
Kind PACKET_KIND `protobuf:"varint,1,opt,name=Kind,proto3,enum=PACKET_KIND" json:"Kind,omitempty"` Kind PACKET_KIND `protobuf:"varint,1,opt,name=Kind,proto3,enum=PACKET_KIND" json:"Kind,omitempty"`
// Types that are assignable to U: // Types that are valid to be assigned to U:
// //
// *Packet_Route // *Packet_Route
// *Packet_Peer // *Packet_Peer
// *Packet_Data // *Packet_Data
// *Packet_Conn
// *Packet_ConnErr
// *Packet_ConnNoti
// *Packet_RptyEvt
// *Packet_RpxEvt
U isPacket_U `protobuf_oneof:"U"` U isPacket_U `protobuf_oneof:"U"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *Packet) Reset() { func (x *Packet) Reset() {
*x = Packet{} *x = Packet{}
mi := &file_hodu_proto_msgTypes[4] mi := &file_hodu_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -464,7 +739,7 @@ func (x *Packet) String() string {
func (*Packet) ProtoMessage() {} func (*Packet) ProtoMessage() {}
func (x *Packet) ProtoReflect() protoreflect.Message { func (x *Packet) ProtoReflect() protoreflect.Message {
mi := &file_hodu_proto_msgTypes[4] mi := &file_hodu_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -477,7 +752,7 @@ func (x *Packet) ProtoReflect() protoreflect.Message {
// Deprecated: Use Packet.ProtoReflect.Descriptor instead. // Deprecated: Use Packet.ProtoReflect.Descriptor instead.
func (*Packet) Descriptor() ([]byte, []int) { func (*Packet) Descriptor() ([]byte, []int) {
return file_hodu_proto_rawDescGZIP(), []int{4} return file_hodu_proto_rawDescGZIP(), []int{9}
} }
func (x *Packet) GetKind() PACKET_KIND { func (x *Packet) GetKind() PACKET_KIND {
@ -487,31 +762,82 @@ func (x *Packet) GetKind() PACKET_KIND {
return PACKET_KIND_RESERVED return PACKET_KIND_RESERVED
} }
func (m *Packet) GetU() isPacket_U { func (x *Packet) GetU() isPacket_U {
if m != nil { if x != nil {
return m.U return x.U
} }
return nil return nil
} }
func (x *Packet) GetRoute() *RouteDesc { func (x *Packet) GetRoute() *RouteDesc {
if x, ok := x.GetU().(*Packet_Route); ok { if x != nil {
if x, ok := x.U.(*Packet_Route); ok {
return x.Route return x.Route
} }
}
return nil return nil
} }
func (x *Packet) GetPeer() *PeerDesc { func (x *Packet) GetPeer() *PeerDesc {
if x, ok := x.GetU().(*Packet_Peer); ok { if x != nil {
if x, ok := x.U.(*Packet_Peer); ok {
return x.Peer return x.Peer
} }
}
return nil return nil
} }
func (x *Packet) GetData() *PeerData { func (x *Packet) GetData() *PeerData {
if x, ok := x.GetU().(*Packet_Data); ok { if x != nil {
if x, ok := x.U.(*Packet_Data); ok {
return x.Data return x.Data
} }
}
return nil
}
func (x *Packet) GetConn() *ConnDesc {
if x != nil {
if x, ok := x.U.(*Packet_Conn); ok {
return x.Conn
}
}
return nil
}
func (x *Packet) GetConnErr() *ConnError {
if x != nil {
if x, ok := x.U.(*Packet_ConnErr); ok {
return x.ConnErr
}
}
return nil
}
func (x *Packet) GetConnNoti() *ConnNotice {
if x != nil {
if x, ok := x.U.(*Packet_ConnNoti); ok {
return x.ConnNoti
}
}
return nil
}
func (x *Packet) GetRptyEvt() *RptyEvent {
if x != nil {
if x, ok := x.U.(*Packet_RptyEvt); ok {
return x.RptyEvt
}
}
return nil
}
func (x *Packet) GetRpxEvt() *RpxEvent {
if x != nil {
if x, ok := x.U.(*Packet_RpxEvt); ok {
return x.RpxEvt
}
}
return nil return nil
} }
@ -531,96 +857,149 @@ type Packet_Data struct {
Data *PeerData `protobuf:"bytes,4,opt,name=Data,proto3,oneof"` Data *PeerData `protobuf:"bytes,4,opt,name=Data,proto3,oneof"`
} }
type Packet_Conn struct {
Conn *ConnDesc `protobuf:"bytes,5,opt,name=Conn,proto3,oneof"`
}
type Packet_ConnErr struct {
ConnErr *ConnError `protobuf:"bytes,6,opt,name=ConnErr,proto3,oneof"`
}
type Packet_ConnNoti struct {
ConnNoti *ConnNotice `protobuf:"bytes,7,opt,name=ConnNoti,proto3,oneof"`
}
type Packet_RptyEvt struct {
RptyEvt *RptyEvent `protobuf:"bytes,8,opt,name=RptyEvt,proto3,oneof"`
}
type Packet_RpxEvt struct {
RpxEvt *RpxEvent `protobuf:"bytes,9,opt,name=RpxEvt,proto3,oneof"`
}
func (*Packet_Route) isPacket_U() {} func (*Packet_Route) isPacket_U() {}
func (*Packet_Peer) isPacket_U() {} func (*Packet_Peer) isPacket_U() {}
func (*Packet_Data) isPacket_U() {} func (*Packet_Data) isPacket_U() {}
func (*Packet_Conn) isPacket_U() {}
func (*Packet_ConnErr) isPacket_U() {}
func (*Packet_ConnNoti) isPacket_U() {}
func (*Packet_RptyEvt) isPacket_U() {}
func (*Packet_RpxEvt) isPacket_U() {}
var File_hodu_proto protoreflect.FileDescriptor var File_hodu_proto protoreflect.FileDescriptor
var file_hodu_proto_rawDesc = []byte{ const file_hodu_proto_rawDesc = "" +
0x0a, 0x0a, 0x68, 0x6f, 0x64, 0x75, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x36, 0x0a, 0x04, "\n" +
0x53, 0x65, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, "\n" +
0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, "hodu.proto\"6\n" +
0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x46, "\x04Seed\x12\x18\n" +
0x6c, 0x61, 0x67, 0x73, 0x22, 0xdf, 0x01, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x44, 0x65, "\aVersion\x18\x01 \x01(\rR\aVersion\x12\x14\n" +
0x73, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, "\x05Flags\x18\x02 \x01(\x04R\x05Flags\"\xdf\x01\n" +
0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0d, "\tRouteDesc\x12\x18\n" +
0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x02, 0x20, "\aRouteId\x18\x01 \x01(\rR\aRouteId\x12$\n" +
0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x53, "\rTargetAddrStr\x18\x02 \x01(\tR\rTargetAddrStr\x12\x1e\n" +
0x74, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, "\n" +
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, "TargetName\x18\x03 \x01(\tR\n" +
0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x70, 0x74, "TargetName\x12$\n" +
0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, "\rServiceOption\x18\x04 \x01(\rR\rServiceOption\x12&\n" +
0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, "\x0eServiceAddrStr\x18\x05 \x01(\tR\x0eServiceAddrStr\x12$\n" +
0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, "\rServiceNetStr\x18\x06 \x01(\tR\rServiceNetStr\"\x86\x01\n" +
0x52, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, "\bPeerDesc\x12\x18\n" +
0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x65, 0x74, 0x53, 0x74, "\aRouteId\x18\x01 \x01(\rR\aRouteId\x12\x16\n" +
0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, "\x06PeerId\x18\x02 \x01(\rR\x06PeerId\x12$\n" +
0x4e, 0x65, 0x74, 0x53, 0x74, 0x72, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x44, "\rRemoteAddrStr\x18\x03 \x01(\tR\rRemoteAddrStr\x12\"\n" +
0x65, 0x73, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01, "\fLocalAddrStr\x18\x04 \x01(\tR\fLocalAddrStr\"P\n" +
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, "\bPeerData\x12\x18\n" +
0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x50, "\aRouteId\x18\x01 \x01(\rR\aRouteId\x12\x16\n" +
0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, "\x06PeerId\x18\x02 \x01(\rR\x06PeerId\x12\x12\n" +
0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65, "\x04Data\x18\x03 \x01(\fR\x04Data\" \n" +
0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x4c, "\bConnDesc\x12\x14\n" +
0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, "\x05Token\x18\x01 \x01(\tR\x05Token\"9\n" +
0x09, 0x52, 0x0c, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x22, "\tConnError\x12\x18\n" +
0x50, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x52, "\aErrorId\x18\x01 \x01(\rR\aErrorId\x12\x12\n" +
0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f, "\x04Text\x18\x02 \x01(\tR\x04Text\" \n" +
0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x18, "\n" +
0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, "ConnNotice\x12\x12\n" +
0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x44, 0x61, 0x74, "\x04Text\x18\x01 \x01(\tR\x04Text\"/\n" +
0x61, 0x22, 0x95, 0x01, 0x0a, 0x06, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x20, 0x0a, 0x04, "\tRptyEvent\x12\x0e\n" +
0x4b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x50, 0x41, 0x43, "\x02Id\x18\x01 \x01(\x04R\x02Id\x12\x12\n" +
0x4b, 0x45, 0x54, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x22, "\x04Data\x18\x02 \x01(\fR\x04Data\".\n" +
0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, "\bRpxEvent\x12\x0e\n" +
0x52, 0x6f, 0x75, 0x74, 0x65, 0x44, 0x65, 0x73, 0x63, 0x48, 0x00, 0x52, 0x05, 0x52, 0x6f, 0x75, "\x02Id\x18\x01 \x01(\x04R\x02Id\x12\x12\n" +
0x74, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, "\x04Data\x18\x02 \x01(\fR\x04Data\"\xd6\x02\n" +
0x32, 0x09, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x48, 0x00, 0x52, 0x04, 0x50, "\x06Packet\x12 \n" +
0x65, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, "\x04Kind\x18\x01 \x01(\x0e2\f.PACKET_KINDR\x04Kind\x12\"\n" +
0x0b, 0x32, 0x09, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x04, "\x05Route\x18\x02 \x01(\v2\n" +
0x44, 0x61, 0x74, 0x61, 0x42, 0x03, 0x0a, 0x01, 0x55, 0x2a, 0x5e, 0x0a, 0x0c, 0x52, 0x4f, 0x55, ".RouteDescH\x00R\x05Route\x12\x1f\n" +
0x54, 0x45, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x4e, 0x53, "\x04Peer\x18\x03 \x01(\v2\t.PeerDescH\x00R\x04Peer\x12\x1f\n" +
0x50, 0x45, 0x43, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, "\x04Data\x18\x04 \x01(\v2\t.PeerDataH\x00R\x04Data\x12\x1f\n" +
0x0a, 0x04, 0x54, 0x43, 0x50, 0x34, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x43, 0x50, 0x36, "\x04Conn\x18\x05 \x01(\v2\t.ConnDescH\x00R\x04Conn\x12&\n" +
0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x10, 0x08, 0x12, 0x08, 0x0a, 0x04, 0x48, "\aConnErr\x18\x06 \x01(\v2\n" +
0x54, 0x54, 0x50, 0x10, 0x10, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x20, ".ConnErrorH\x00R\aConnErr\x12)\n" +
0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, 0x10, 0x40, 0x2a, 0xb5, 0x01, 0x0a, 0x0b, 0x50, 0x41, "\bConnNoti\x18\a \x01(\v2\v.ConnNoticeH\x00R\bConnNoti\x12&\n" +
0x43, 0x4b, 0x45, 0x54, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, "\aRptyEvt\x18\b \x01(\v2\n" +
0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x4f, 0x55, 0x54, 0x45, ".RptyEventH\x00R\aRptyEvt\x12#\n" +
0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x4f, 0x55, 0x54, "\x06RpxEvt\x18\t \x01(\v2\t.RpxEventH\x00R\x06RpxEvtB\x03\n" +
0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54, "\x01U*U\n" +
0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x52, "\fROUTE_OPTION\x12\n" +
0x4f, 0x55, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x04, 0x12, 0x10, "\n" +
0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x05, "\x06UNSPEC\x10\x00\x12\a\n" +
0x12, 0x10, 0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, "\x03TCP\x10\x01\x12\b\n" +
0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x41, 0x42, 0x4f, 0x52, 0x54, "\x04TCP4\x10\x02\x12\b\n" +
0x45, 0x44, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x45, 0x4f, 0x46, "\x04TCP6\x10\x04\x12\b\n" +
0x10, 0x08, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x10, "\x04HTTP\x10\b\x12\t\n" +
0x09, 0x32, 0x49, 0x0a, 0x04, 0x48, 0x6f, 0x64, 0x75, 0x12, 0x19, 0x0a, 0x07, 0x47, 0x65, 0x74, "\x05HTTPS\x10\x10\x12\a\n" +
0x53, 0x65, 0x65, 0x64, 0x12, 0x05, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x1a, 0x05, 0x2e, 0x53, 0x65, "\x03SSH\x10 *\xda\x02\n" +
0x65, 0x64, 0x22, 0x00, 0x12, 0x26, 0x0a, 0x0c, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x74, "\vPACKET_KIND\x12\f\n" +
0x72, 0x65, 0x61, 0x6d, 0x12, 0x07, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x1a, 0x07, 0x2e, "\bRESERVED\x10\x00\x12\x0f\n" +
0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, "\vROUTE_START\x10\x01\x12\x0e\n" +
0x2e, 0x2f, 0x68, 0x6f, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, "\n" +
} "ROUTE_STOP\x10\x02\x12\x11\n" +
"\rROUTE_STARTED\x10\x03\x12\x11\n" +
"\rROUTE_STOPPED\x10\x04\x12\x10\n" +
"\fPEER_STARTED\x10\x05\x12\x10\n" +
"\fPEER_STOPPED\x10\x06\x12\x10\n" +
"\fPEER_ABORTED\x10\a\x12\f\n" +
"\bPEER_EOF\x10\b\x12\r\n" +
"\tPEER_DATA\x10\t\x12\r\n" +
"\tCONN_DESC\x10\v\x12\x0e\n" +
"\n" +
"CONN_ERROR\x10\f\x12\x0f\n" +
"\vCONN_NOTICE\x10\r\x12\x0e\n" +
"\n" +
"RPTY_START\x10\x0e\x12\r\n" +
"\tRPTY_STOP\x10\x0f\x12\r\n" +
"\tRPTY_DATA\x10\x10\x12\r\n" +
"\tRPTY_SIZE\x10\x11\x12\r\n" +
"\tRPX_START\x10\x12\x12\f\n" +
"\bRPX_STOP\x10\x13\x12\f\n" +
"\bRPX_DATA\x10\x14\x12\v\n" +
"\aRPX_EOF\x10\x152I\n" +
"\x04Hodu\x12\x19\n" +
"\aGetSeed\x12\x05.Seed\x1a\x05.Seed\"\x00\x12&\n" +
"\fPacketStream\x12\a.Packet\x1a\a.Packet\"\x00(\x010\x01B\bZ\x06./hodub\x06proto3"
var ( var (
file_hodu_proto_rawDescOnce sync.Once file_hodu_proto_rawDescOnce sync.Once
file_hodu_proto_rawDescData = file_hodu_proto_rawDesc file_hodu_proto_rawDescData []byte
) )
func file_hodu_proto_rawDescGZIP() []byte { func file_hodu_proto_rawDescGZIP() []byte {
file_hodu_proto_rawDescOnce.Do(func() { file_hodu_proto_rawDescOnce.Do(func() {
file_hodu_proto_rawDescData = protoimpl.X.CompressGZIP(file_hodu_proto_rawDescData) file_hodu_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hodu_proto_rawDesc), len(file_hodu_proto_rawDesc)))
}) })
return file_hodu_proto_rawDescData return file_hodu_proto_rawDescData
} }
var file_hodu_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_hodu_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_hodu_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_hodu_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_hodu_proto_goTypes = []any{ var file_hodu_proto_goTypes = []any{
(ROUTE_OPTION)(0), // 0: ROUTE_OPTION (ROUTE_OPTION)(0), // 0: ROUTE_OPTION
(PACKET_KIND)(0), // 1: PACKET_KIND (PACKET_KIND)(0), // 1: PACKET_KIND
@ -628,22 +1007,32 @@ var file_hodu_proto_goTypes = []any{
(*RouteDesc)(nil), // 3: RouteDesc (*RouteDesc)(nil), // 3: RouteDesc
(*PeerDesc)(nil), // 4: PeerDesc (*PeerDesc)(nil), // 4: PeerDesc
(*PeerData)(nil), // 5: PeerData (*PeerData)(nil), // 5: PeerData
(*Packet)(nil), // 6: Packet (*ConnDesc)(nil), // 6: ConnDesc
(*ConnError)(nil), // 7: ConnError
(*ConnNotice)(nil), // 8: ConnNotice
(*RptyEvent)(nil), // 9: RptyEvent
(*RpxEvent)(nil), // 10: RpxEvent
(*Packet)(nil), // 11: Packet
} }
var file_hodu_proto_depIdxs = []int32{ var file_hodu_proto_depIdxs = []int32{
1, // 0: Packet.Kind:type_name -> PACKET_KIND 1, // 0: Packet.Kind:type_name -> PACKET_KIND
3, // 1: Packet.Route:type_name -> RouteDesc 3, // 1: Packet.Route:type_name -> RouteDesc
4, // 2: Packet.Peer:type_name -> PeerDesc 4, // 2: Packet.Peer:type_name -> PeerDesc
5, // 3: Packet.Data:type_name -> PeerData 5, // 3: Packet.Data:type_name -> PeerData
2, // 4: Hodu.GetSeed:input_type -> Seed 6, // 4: Packet.Conn:type_name -> ConnDesc
6, // 5: Hodu.PacketStream:input_type -> Packet 7, // 5: Packet.ConnErr:type_name -> ConnError
2, // 6: Hodu.GetSeed:output_type -> Seed 8, // 6: Packet.ConnNoti:type_name -> ConnNotice
6, // 7: Hodu.PacketStream:output_type -> Packet 9, // 7: Packet.RptyEvt:type_name -> RptyEvent
6, // [6:8] is the sub-list for method output_type 10, // 8: Packet.RpxEvt:type_name -> RpxEvent
4, // [4:6] is the sub-list for method input_type 2, // 9: Hodu.GetSeed:input_type -> Seed
4, // [4:4] is the sub-list for extension type_name 11, // 10: Hodu.PacketStream:input_type -> Packet
4, // [4:4] is the sub-list for extension extendee 2, // 11: Hodu.GetSeed:output_type -> Seed
0, // [0:4] is the sub-list for field type_name 11, // 12: Hodu.PacketStream:output_type -> Packet
11, // [11:13] is the sub-list for method output_type
9, // [9:11] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
} }
func init() { file_hodu_proto_init() } func init() { file_hodu_proto_init() }
@ -651,18 +1040,23 @@ func file_hodu_proto_init() {
if File_hodu_proto != nil { if File_hodu_proto != nil {
return return
} }
file_hodu_proto_msgTypes[4].OneofWrappers = []any{ file_hodu_proto_msgTypes[9].OneofWrappers = []any{
(*Packet_Route)(nil), (*Packet_Route)(nil),
(*Packet_Peer)(nil), (*Packet_Peer)(nil),
(*Packet_Data)(nil), (*Packet_Data)(nil),
(*Packet_Conn)(nil),
(*Packet_ConnErr)(nil),
(*Packet_ConnNoti)(nil),
(*Packet_RptyEvt)(nil),
(*Packet_RpxEvt)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_hodu_proto_rawDesc, RawDescriptor: unsafe.Slice(unsafe.StringData(file_hodu_proto_rawDesc), len(file_hodu_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 5, NumMessages: 10,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
@ -672,7 +1066,6 @@ func file_hodu_proto_init() {
MessageInfos: file_hodu_proto_msgTypes, MessageInfos: file_hodu_proto_msgTypes,
}.Build() }.Build()
File_hodu_proto = out.File File_hodu_proto = out.File
file_hodu_proto_rawDesc = nil
file_hodu_proto_goTypes = nil file_hodu_proto_goTypes = nil
file_hodu_proto_depIdxs = nil file_hodu_proto_depIdxs = nil
} }

View File

@ -23,35 +23,34 @@ enum ROUTE_OPTION {
TCP = 1; TCP = 1;
TCP4 = 2; TCP4 = 2;
TCP6 = 4; TCP6 = 4;
TTY = 8; HTTP = 8;
HTTP = 16; HTTPS = 16;
HTTPS = 32; SSH = 32;
SSH = 64;
}; };
message RouteDesc { message RouteDesc {
uint32 RouteId = 1; uint32 RouteId = 1;
// C->S(ROUTE_START): client-side peer address // C->S(ROUTE_START/STOP): client-side peer address
// S->C(ROUTE_STARTED): server-side listening address // S->C(ROUTE_STARTED/STOPPED): server-side listening address
string TargetAddrStr = 2; string TargetAddrStr = 2;
// C->S(ROUTE_START): human-readable name of client-side peer // C->S(ROUTE_START/STOPPED): human-readable name of client-side peer
// S->C(ROUTE_STARTED): clone as sent by C // S->C(ROUTE_STARTED/STOPPED): clone as sent by C
string TargetName= 3; string TargetName= 3;
// C->S(ROUTE_START): desired listening option on the server-side(e.g. tcp, tcp4, tcp6) + // C->S(ROUTE_START): requested listening option on the server-side(e.g. tcp, tcp4, tcp6) +
// hint to the service-side peer(e.g. local) + // hint to the service-side peer(e.g. local) +
// hint to the client-side peer(e.g. tty, http, https) // hint to the client-side peer(e.g. tty, http, https)
// S->C(ROUTE_STARTED): cloned as sent by C. // S->C(ROUTE_STARTED): cloned as sent by C.
uint32 ServiceOption = 4; uint32 ServiceOption = 4;
// C->S(ROUTE_START): desired lisening address on the service-side // C->S(ROUTE_START): requested lisening address on the service-side
// S->C(ROUTE_STARTED): cloned as sent by C // S->C(ROUTE_STARTED): cloned as sent by C
string ServiceAddrStr = 5; string ServiceAddrStr = 5;
// C->S(ROUTE_START): permitted network of server-side peers. // C->S(ROUTE_START): requested permitted network of server-side peers.
// S->C(ROUTE_STARTED): cloned as sent by C. // S->C(ROUTE_STARTED): actual permitted network of server-side peers
string ServiceNetStr = 6; string ServiceNetStr = 6;
}; };
@ -68,6 +67,29 @@ message PeerData {
bytes Data = 3; bytes Data = 3;
}; };
message ConnDesc {
string Token = 1;
};
message ConnError {
uint32 ErrorId = 1;
string Text = 2;
};
message ConnNotice {
string Text = 1;
};
message RptyEvent {
uint64 Id = 1;
bytes Data = 2;
};
message RpxEvent {
uint64 Id = 1;
bytes Data = 2;
};
enum PACKET_KIND { enum PACKET_KIND {
RESERVED = 0; // not used RESERVED = 0; // not used
ROUTE_START = 1; ROUTE_START = 1;
@ -79,6 +101,19 @@ enum PACKET_KIND {
PEER_ABORTED = 7; PEER_ABORTED = 7;
PEER_EOF = 8; PEER_EOF = 8;
PEER_DATA = 9; PEER_DATA = 9;
CONN_DESC = 11;
CONN_ERROR = 12;
CONN_NOTICE = 13;
RPTY_START = 14;
RPTY_STOP = 15;
RPTY_DATA = 16;
RPTY_SIZE = 17; // terminal size
RPX_START = 18;
RPX_STOP = 19;
RPX_DATA = 20;
RPX_EOF = 21;
}; };
message Packet { message Packet {
@ -88,5 +123,10 @@ message Packet {
RouteDesc Route = 2; RouteDesc Route = 2;
PeerDesc Peer = 3; PeerDesc Peer = 3;
PeerData Data = 4; PeerData Data = 4;
ConnDesc Conn = 5;
ConnError ConnErr = 6;
ConnNotice ConnNoti = 7;
RptyEvent RptyEvt = 8;
RpxEvent RpxEvt = 9;
}; };
} }

134
jwt.go Normal file
View File

@ -0,0 +1,134 @@
package hodu
import "crypto"
//import "crypto/hmac"
import "crypto/rand"
import "crypto/rsa"
import "encoding/base64"
import "encoding/json"
import "fmt"
import "hash"
import "strings"
/*
func Sign(data []byte, privkey *rsa.PrivateKey) ([]byte, error) {
var h hash.Hash
h = crypto.SHA512.New()
h.Write(data)
//fmt.Printf("%+v\n", h.Sum(nil))
return rsa.SignPKCS1v15(rand.Reader, privkey, crypto.SHA512, h.Sum(nil))
}
func Verify(data []byte, pubkey *rsa.PublicKey, sig []byte) error {
var h hash.Hash
h = crypto.SHA512.New()
h.Write(data)
return rsa.VerifyPKCS1v15(pubkey, crypto.SHA512, h.Sum(nil), sig)
}
func SignHS512(data []byte, key string) ([]byte, error) {
var h hash.Hash
h = hmac.New(crypto.SHA512.New, []byte(key))
h.Write(data)
return h.Sum(nil), nil
}
func VerifyHS512(data []byte, key string, sig []byte) error {
var h hash.Hash
h = crypto.SHA512.New()
h.Write(data)
if !hmac.Equal(h.Sum(nil), sig) { return fmt.Errorf("invalid signature") }
return nil
}
*/
type JWT[T any] struct {
key *rsa.PrivateKey
claims *T
}
type JWTHeader struct {
Algo string `json:"alg"`
Type string `json:"typ"`
}
type JWTClaimMap map[string]interface{}
func NewJWT[T any](key *rsa.PrivateKey, claims *T) *JWT[T] {
return &JWT[T]{key: key, claims: claims}
}
func (j *JWT[T]) SignRS512() (string, error) {
var h JWTHeader
var hb []byte
var cb []byte
var ss string
var sb []byte
var hs hash.Hash
var err error
h.Algo = "RS512"
h.Type = "JWT"
hb, err = json.Marshal(h)
if err != nil { return "", err }
cb, err = json.Marshal(j.claims)
if err != nil { return "", err }
ss = base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(cb)
hs = crypto.SHA512.New()
hs.Write([]byte(ss))
sb, err = rsa.SignPKCS1v15(rand.Reader, j.key, crypto.SHA512, hs.Sum(nil))
if err != nil { return "", err }
//fmt.Printf ("%+v %+v %s\n", string(hb), string(cb), (ss + "." + base64.RawURLEncoding.EncodeToString(sb)))
return ss + "." + base64.RawURLEncoding.EncodeToString(sb), nil
}
func (j *JWT[T]) VerifyRS512(tok string) error {
var segs []string
var hb []byte
var cb []byte
var ss []byte
var jh JWTHeader
var hs hash.Hash
var err error
segs = strings.Split(tok, ".")
if len(segs) != 3 { return fmt.Errorf("invalid token") }
hb, err = base64.RawURLEncoding.DecodeString(segs[0])
if err != nil { return fmt.Errorf("invalid header - %s", err.Error()) }
err = json.Unmarshal(hb, &jh)
if err != nil { return fmt.Errorf("invalid header - %s", err.Error()) }
if jh.Algo != "RS512" || jh.Type != "JWT" { return fmt.Errorf("invalid header content %+v", jh) }
cb, err = base64.RawURLEncoding.DecodeString(segs[1])
if err != nil { return fmt.Errorf("invalid claims - %s", err.Error()) }
err = json.Unmarshal(cb, j.claims)
if err != nil { return fmt.Errorf("invalid claims - %s", err.Error()) }
ss, err = base64.RawURLEncoding.DecodeString(segs[2])
if err != nil { return fmt.Errorf("invalid signature - %s", err.Error()) }
hs = crypto.SHA512.New()
hs.Write([]byte(segs[0]))
hs.Write([]byte("."))
hs.Write([]byte(segs[1]))
err = rsa.VerifyPKCS1v15(&j.key.PublicKey, crypto.SHA512, hs.Sum(nil), ss)
if err != nil { return fmt.Errorf("unverifiable signature - %s", err.Error()) }
return nil
}

39
jwt_test.go Normal file
View File

@ -0,0 +1,39 @@
package hodu_test
import "crypto/rand"
import "crypto/rsa"
import "hodu"
import "testing"
func TestJwt(t *testing.T) {
var tok string
var err error
type JWTClaim struct {
Abc string `json:"abc"`
Donkey string `json:"donkey"`
IssuedAt int `json:"iat"`
}
var jc JWTClaim
jc.Abc = "def"
jc.Donkey = "kong"
jc.IssuedAt = 111
var key *rsa.PrivateKey
key, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil { t.Fatalf("keygen failure - %s", err.Error()) }
var j *hodu.JWT[JWTClaim]
j = hodu.NewJWT(key, &jc)
tok, err = j.SignRS512()
if err != nil { t.Fatalf("signing failure - %s", err.Error()) }
jc = JWTClaim{}
err = j.VerifyRS512(tok)
if err != nil { t.Fatalf("verification failure - %s", err.Error()) }
if jc.Abc != "def" { t.Fatal("decoding failure of Abc field") }
if jc.Donkey != "kong" { t.Fatal("decoding failure of Donkey field") }
if jc.IssuedAt != 111 { t.Fatal("decoding failure of Issued field") }
}

View File

@ -62,3 +62,50 @@ func MakePeerDataPacket(route_id RouteId, peer_id PeerId, data []byte) *Packet {
return &Packet{Kind: PACKET_KIND_PEER_DATA, return &Packet{Kind: PACKET_KIND_PEER_DATA,
U: &Packet_Data{Data: &PeerData{RouteId: uint32(route_id), PeerId: uint32(peer_id), Data: data}}} U: &Packet_Data{Data: &PeerData{RouteId: uint32(route_id), PeerId: uint32(peer_id), Data: data}}}
} }
func MakeConnDescPacket(token string) *Packet {
return &Packet{Kind: PACKET_KIND_CONN_DESC,U: &Packet_Conn{Conn: &ConnDesc{Token: token}}}
}
func MakeConnErrorPacket(error_id uint32, msg string) *Packet {
return &Packet{Kind: PACKET_KIND_CONN_ERROR, U: &Packet_ConnErr{ConnErr: &ConnError{ErrorId: error_id, Text: msg}}}
}
func MakeConnNoticePacket(msg string) *Packet {
return &Packet{Kind: PACKET_KIND_CONN_NOTICE, U: &Packet_ConnNoti{ConnNoti: &ConnNotice{Text: msg}}}
}
func MakeRptyStartPacket(id uint64) *Packet {
return &Packet{Kind: PACKET_KIND_RPTY_START, U: &Packet_RptyEvt{RptyEvt: &RptyEvent{Id: id}}}
}
func MakeRptyStopPacket(id uint64, msg string) *Packet {
// the rpty stop conveys an error/info message
return &Packet{Kind: PACKET_KIND_RPTY_STOP, U: &Packet_RptyEvt{RptyEvt: &RptyEvent{Id: id, Data: []byte(msg)}}}
}
func MakeRptyDataPacket(id uint64, data []byte) *Packet {
return &Packet{Kind: PACKET_KIND_RPTY_DATA, U: &Packet_RptyEvt{RptyEvt: &RptyEvent{Id: id, Data: data}}}
}
func MakeRptySizePacket(id uint64, data []byte) *Packet {
return &Packet{Kind: PACKET_KIND_RPTY_SIZE, U: &Packet_RptyEvt{RptyEvt: &RptyEvent{Id: id, Data: data}}}
}
func MakeRpxStartPacket(id uint64, hdr_part []byte) *Packet {
// the rpx start conveys the data unlike other Start packets...
return &Packet{Kind: PACKET_KIND_RPX_START, U: &Packet_RpxEvt{RpxEvt: &RpxEvent{Id: id, Data: hdr_part}}}
}
func MakeRpxStopPacket(id uint64) *Packet {
// the rpx start conveys the data unlike other Start packets...
return &Packet{Kind: PACKET_KIND_RPX_STOP, U: &Packet_RpxEvt{RpxEvt: &RpxEvent{Id: id}}}
}
func MakeRpxDataPacket(id uint64, data_part []byte) *Packet {
return &Packet{Kind: PACKET_KIND_RPX_DATA, U: &Packet_RpxEvt{RpxEvt: &RpxEvent{Id: id, Data: data_part}}}
}
func MakeRpxEofPacket(id uint64) *Packet {
return &Packet{Kind: PACKET_KIND_RPX_EOF, U: &Packet_RpxEvt{RpxEvt: &RpxEvent{Id: id}}}
}

71
pty.go Normal file
View File

@ -0,0 +1,71 @@
package hodu
import "encoding/json"
import "fmt"
import "os"
import "os/exec"
import "os/user"
import "strconv"
import "syscall"
import pts "github.com/creack/pty"
import "golang.org/x/net/websocket"
import "golang.org/x/sys/unix"
func connect_pty(pty_shell string, pty_user string) (*exec.Cmd, *os.File, error) {
var cmd *exec.Cmd
var tty *os.File
var err error
if pty_shell == "" {
return nil, nil, fmt.Errorf("blank pty shell")
}
cmd = exec.Command(pty_shell)
if pty_user != "" {
var uid int
var gid int
var u *user.User
u, err = user.Lookup(pty_user)
if err != nil { return nil, nil, err }
uid, _ = strconv.Atoi(u.Uid)
gid, _ = strconv.Atoi(u.Gid)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
Setsid: true,
}
cmd.Dir = u.HomeDir
cmd.Env = append(cmd.Env,
"HOME=" + u.HomeDir,
"LOGNAME=" + u.Username,
"PATH=" + os.Getenv("PATH"),
"SHELL=" + pty_shell,
"TERM=xterm",
"USER=" + u.Username,
)
}
tty, err = pts.Start(cmd)
if err != nil {
return nil, nil, err
}
//syscall.SetNonblock(int(tty.Fd()), true)
unix.SetNonblock(int(tty.Fd()), true)
return cmd, tty, nil
}
func send_ws_data_for_xterm(ws *websocket.Conn, type_val string, data string) error {
var msg []byte
var err error
msg, err = json.Marshal(json_xterm_ws_event{Type: type_val, Data: []string{ data } })
if err == nil { err = websocket.Message.Send(ws, msg) }
return err
}

File diff suppressed because it is too large Load Diff

139
server-metrics.go Normal file
View File

@ -0,0 +1,139 @@
package hodu
import "runtime"
import "strings"
import "github.com/prometheus/client_golang/prometheus"
type ServerCollector struct {
server *Server
BuildInfo *prometheus.Desc
ServerConns *prometheus.Desc
ServerRoutes *prometheus.Desc
ServerPeers *prometheus.Desc
SshProxySessions *prometheus.Desc
PtySessions *prometheus.Desc
RptySessions *prometheus.Desc
RpxSessions *prometheus.Desc
}
// NewServerCollector returns a new ServerCollector with all prometheus.Desc initialized
func NewServerCollector(server *Server) ServerCollector {
var prefix string
// prometheus doesn't like a dash. change it to an underscore
prefix = strings.ReplaceAll(server.Name(), "-", "_") + "_"
return ServerCollector{
server: server,
BuildInfo: prometheus.NewDesc(
prefix + "build_info",
"Build information",
[]string{
"goarch",
"goos",
"goversion",
}, nil,
),
ServerConns: prometheus.NewDesc(
prefix + "server_conns",
"Number of server connections from clients",
nil, nil,
),
ServerRoutes: prometheus.NewDesc(
prefix + "server_routes",
"Number of server-side routes",
nil, nil,
),
ServerPeers: prometheus.NewDesc(
prefix + "server_peers",
"Number of server-side peers",
nil, nil,
),
SshProxySessions: prometheus.NewDesc(
prefix + "pxy_ssh_sessions",
"Number of SSH proxy sessions",
nil, nil,
),
PtySessions: prometheus.NewDesc(
prefix + "pty_sessions",
"Number of pty session",
nil, nil,
),
RptySessions: prometheus.NewDesc(
prefix + "rpty_sessions",
"Number of rpty session",
nil, nil,
),
RpxSessions: prometheus.NewDesc(
prefix + "rpx_sessions",
"Number of rpx session",
nil, nil,
),
}
}
func (c ServerCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.BuildInfo
ch <- c.ServerConns
ch <- c.ServerRoutes
ch <- c.ServerPeers
ch <- c.SshProxySessions
ch <- c.PtySessions
ch <- c.RptySessions
ch <- c.RpxSessions
}
func (c ServerCollector) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
c.BuildInfo,
prometheus.GaugeValue,
1,
runtime.GOARCH,
runtime.GOOS,
runtime.Version(),
)
ch <- prometheus.MustNewConstMetric(
c.ServerConns,
prometheus.GaugeValue,
float64(c.server.stats.conns.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.ServerRoutes,
prometheus.GaugeValue,
float64(c.server.stats.routes.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.ServerPeers,
prometheus.GaugeValue,
float64(c.server.stats.peers.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.SshProxySessions,
prometheus.GaugeValue,
float64(c.server.stats.ssh_proxy_sessions.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.PtySessions,
prometheus.GaugeValue,
float64(c.server.stats.pty_sessions.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.RptySessions,
prometheus.GaugeValue,
float64(c.server.stats.rpty_sessions.Load()),
)
ch <- prometheus.MustNewConstMetric(
c.RpxSessions,
prometheus.GaugeValue,
float64(c.server.stats.rpx_sessions.Load()),
)
}

View File

@ -1,5 +1,6 @@
package hodu package hodu
import "container/list"
import "context" import "context"
import "errors" import "errors"
import "io" import "io"
@ -12,8 +13,11 @@ import "time"
type ServerPeerConn struct { type ServerPeerConn struct {
route *ServerRoute route *ServerRoute
conn_id PeerId conn_id PeerId
cts *ClientConn
conn *net.TCPConn conn *net.TCPConn
Created time.Time
node_in_server *list.Element
node_in_conn *list.Element
stop_chan chan bool stop_chan chan bool
stop_req atomic.Bool stop_req atomic.Bool
@ -22,6 +26,8 @@ type ServerPeerConn struct {
client_peer_started atomic.Bool client_peer_started atomic.Bool
client_peer_stopped atomic.Bool client_peer_stopped atomic.Bool
client_peer_eof atomic.Bool client_peer_eof atomic.Bool
client_peer_laddr Atom[string]
client_peer_raddr Atom[string]
} }
func NewServerPeerConn(r *ServerRoute, c *net.TCPConn, id PeerId) *ServerPeerConn { func NewServerPeerConn(r *ServerRoute, c *net.TCPConn, id PeerId) *ServerPeerConn {
@ -30,6 +36,7 @@ func NewServerPeerConn(r *ServerRoute, c *net.TCPConn, id PeerId) *ServerPeerCon
spc.route = r spc.route = r
spc.conn = c spc.conn = c
spc.conn_id = id spc.conn_id = id
spc.Created = time.Now()
spc.stop_chan = make(chan bool, 8) spc.stop_chan = make(chan bool, 8)
spc.stop_req.Store(false) spc.stop_req.Store(false)
@ -54,21 +61,23 @@ func (spc *ServerPeerConn) RunTask(wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
spc.route.Cts.S.FirePeerEvent(SERVER_EVENT_PEER_STARTED, spc)
conn_raddr = spc.conn.RemoteAddr().String() conn_raddr = spc.conn.RemoteAddr().String()
conn_laddr = spc.conn.LocalAddr().String() conn_laddr = spc.conn.LocalAddr().String()
pss = spc.route.Cts.pss pss = spc.route.Cts.pss
err = pss.Send(MakePeerStartedPacket(spc.route.Id, spc.conn_id, conn_raddr, conn_laddr)) err = pss.Send(MakePeerStartedPacket(spc.route.Id, spc.conn_id, conn_raddr, conn_laddr))
if err != nil { if err != nil {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to send peer_started event(%d,%d,%s,%s) to client - %s", "Failed to send %s event(%d,%d,%s,%s) to client - %s",
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error()) PACKET_KIND_PEER_STARTED.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
goto done_without_stop goto done_without_stop
} }
// set up a timer to set waiting duration until the connection is // set up a timer to set waiting duration until the connection is
// actually established on the client side and it's informed... // actually established on the client side and it's informed...
waitctx, cancel_wait = context.WithTimeout(spc.route.Cts.svr.ctx, 5 * time.Second) // TODO: make this configurable waitctx, cancel_wait = context.WithTimeout(spc.route.Cts.S.Ctx, 5 * time.Second) // TODO: make this configurable
wait_for_started: wait_for_started:
for { for {
select { select {
@ -93,31 +102,33 @@ wait_for_started:
for { for {
n, err = spc.conn.Read(buf[:]) n, err = spc.conn.Read(buf[:])
if n > 0 {
var err2 error
err2 = pss.Send(MakePeerDataPacket(spc.route.Id, spc.conn_id, buf[:n]))
if err2 != nil {
spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to send %s from peer(%d,%d,%s,%s) to client - %s",
PACKET_KIND_PEER_DATA.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err2.Error())
goto done
}
}
if err != nil { if err != nil {
if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") { // i don't like this way to check this error. if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") { // i don't like this way to check this error.
err = pss.Send(MakePeerEofPacket(spc.route.Id, spc.conn_id)) err = pss.Send(MakePeerEofPacket(spc.route.Id, spc.conn_id))
if err != nil { if err != nil {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to send peer_eof event(%d,%d,%s,%s) to client - %s", "Failed to send %s event(%d,%d,%s,%s) to client - %s",
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error()) PACKET_KIND_PEER_EOF.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
goto done goto done
} }
goto wait_for_stopped goto wait_for_stopped
} else { } else {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to read data from peer(%d,%d,%s,%s) - %s", "Failed to read data from peer(%d,%d,%s,%s) - %s",
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error()) spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
goto done goto done
} }
} }
err = pss.Send(MakePeerDataPacket(spc.route.Id, spc.conn_id, buf[:n]))
if err != nil {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR,
"Failed to send data from peer(%d,%d,%s,%s) to client - %s",
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
goto done
}
} }
wait_for_stopped: wait_for_stopped:
@ -133,15 +144,27 @@ wait_for_stopped:
done: done:
err = pss.Send(MakePeerStoppedPacket(spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), spc.conn.LocalAddr().String())) err = pss.Send(MakePeerStoppedPacket(spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), spc.conn.LocalAddr().String()))
if err != nil { if err != nil {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to send peer_stopped(%d,%d,%s,%s) to client - %s", "Failed to send %s(%d,%d,%s,%s) to client - %s",
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error()) PACKET_KIND_PEER_STOPPED.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
// nothing much to do about the failure of sending this // nothing much to do about the failure of sending this
} }
done_without_stop: done_without_stop:
spc.ReqStop() spc.ReqStop()
spc.route.RemoveServerPeerConn(spc) spc.route.RemoveServerPeerConn(spc)
spc.route.Cts.S.pts_mtx.Lock()
spc.route.Cts.S.pts_list.Remove(spc.node_in_server)
spc.node_in_server = nil
spc.route.Cts.S.pts_mtx.Unlock()
spc.route.Cts.pts_mtx.Lock()
spc.route.Cts.pts_list.Remove(spc.node_in_conn)
spc.node_in_conn = nil
spc.route.Cts.pts_mtx.Unlock()
spc.route.Cts.S.FirePeerEvent(SERVER_EVENT_PEER_STOPPED, spc)
} }
func (spc *ServerPeerConn) ReqStop() { func (spc *ServerPeerConn) ReqStop() {
@ -159,14 +182,29 @@ func (spc *ServerPeerConn) ReqStop() {
} }
} }
func (spc *ServerPeerConn) ReportEvent(event_type PACKET_KIND, event_data interface{}) error { func (spc *ServerPeerConn) ReportPacket(packet_type PACKET_KIND, event_data interface{}) error {
switch event_type { switch packet_type {
case PACKET_KIND_PEER_STARTED: case PACKET_KIND_PEER_STARTED:
var ok bool
var pd *PeerDesc
pd, ok = event_data.(*PeerDesc)
if !ok {
// something wrong. leave it unknown.
spc.client_peer_laddr.Set("")
spc.client_peer_raddr.Set("")
} else {
spc.client_peer_laddr.Set(pd.LocalAddrStr)
spc.client_peer_raddr.Set(pd.RemoteAddrStr)
}
if spc.client_peer_started.CompareAndSwap(false, true) { if spc.client_peer_started.CompareAndSwap(false, true) {
spc.client_peer_status_chan <- true spc.client_peer_status_chan <- true
} }
spc.route.Cts.S.FirePeerEvent(SERVER_EVENT_PEER_UPDATED, spc)
case PACKET_KIND_PEER_ABORTED: case PACKET_KIND_PEER_ABORTED:
spc.ReqStop() spc.ReqStop()
@ -192,21 +230,21 @@ func (spc *ServerPeerConn) ReportEvent(event_type PACKET_KIND, event_data interf
var err error var err error
_, err = spc.conn.Write(data) _, err = spc.conn.Write(data)
if err != nil { if err != nil {
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Failed to write data from %s to peer(%d,%d,%s) - %s", "Failed to write data from %s to peer(%d,%d,%s) - %s",
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), err.Error()) spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), err.Error())
spc.ReqStop() spc.ReqStop()
} }
} else { } else {
// this must not happen. // this must not happen.
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Protocol error - invalid data in peer_data event from %s to peer(%d,%d,%s)", "Protocol error - invalid data in peer_data event from %s to peer(%d,%d,%s)",
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String()) spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String())
spc.ReqStop() spc.ReqStop()
} }
} else { } else {
// protocol error. the client must not relay more data from the client-side peer after EOF. // protocol error. the client must not relay more data from the client-side peer after EOF.
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR, spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
"Protocol error - redundant data from %s to (%d,%d,%s)", "Protocol error - redundant data from %s to (%d,%d,%s)",
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String()) spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String())
spc.ReqStop() spc.ReqStop()

400
server-pty.go Normal file
View File

@ -0,0 +1,400 @@
package hodu
import "encoding/json"
import "errors"
import "fmt"
import "io"
import "net/http"
import "os"
import "os/exec"
import "strconv"
import "strings"
import "sync"
import "text/template"
import pts "github.com/creack/pty"
import "golang.org/x/net/websocket"
import "golang.org/x/sys/unix"
type server_pty_ws struct {
S *Server
Id string
ws *websocket.Conn
}
type server_rpty_ws struct {
S *Server
Id string
ws *websocket.Conn
}
type server_pty_xterm_file struct {
ServerCtl
file string
mode string
}
// ------------------------------------------------------
func (pty *server_pty_ws) Identity() string {
return pty.Id
}
func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
var s *Server
var req *http.Request
//var username string
//var password string
var in *os.File
var out *os.File
var tty *os.File
var cmd *exec.Cmd
var pfd [2]int = [2]int{ -1, -1 }
var wg sync.WaitGroup
var conn_ready_chan chan bool
var err error
s = pty.S
req = ws.Request()
conn_ready_chan = make(chan bool, 3)
wg.Add(1)
go func() {
var conn_ready bool
defer wg.Done()
defer ws.Close() // dirty way to break the main loop
conn_ready = <-conn_ready_chan
if conn_ready { // connected
var poll_fds []unix.PollFd
var buf [2048]byte
var n int
var err error
poll_fds = []unix.PollFd{
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
}
s.stats.pty_sessions.Add(1)
for {
n, err = unix.Poll(poll_fds, -1) // -1 means wait indefinitely
if err != nil {
if errors.Is(err, unix.EINTR) { continue }
s.log.Write("", LOG_ERROR, "[%s] Failed to poll pty stdout - %s", req.RemoteAddr, err.Error())
break
}
if n == 0 { // timed out
continue
}
if (poll_fds[0].Revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
s.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty stdout", req.RemoteAddr)
break
}
if (poll_fds[1].Revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
s.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty event pipe", req.RemoteAddr)
break
}
if (poll_fds[0].Revents & unix.POLLIN) != 0 {
n, err = out.Read(buf[:])
if n > 0 {
var err2 error
err2 = send_ws_data_for_xterm(ws, "iov", string(buf[:n]))
if err2 != nil {
s.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to send to websocket - %s", req.RemoteAddr, err2.Error())
break
}
}
if err != nil {
if !errors.Is(err, io.EOF) {
s.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to read pty stdout - %s", req.RemoteAddr, err.Error())
}
break
}
}
if (poll_fds[1].Revents & unix.POLLIN) != 0 {
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Stop request noticed on pty event pipe", req.RemoteAddr)
break
}
}
s.stats.pty_sessions.Add(-1)
}
}()
ws_recv_loop:
for {
var msg []byte
err = websocket.Message.Receive(ws, &msg)
if err != nil { goto done }
if len(msg) > 0 {
var ev json_xterm_ws_event
err = json.Unmarshal(msg, &ev)
if err == nil {
switch ev.Type {
case "open":
if tty == nil && len(ev.Data) == 2 {
// not using username and password for now...
//username = string(ev.Data[0])
//password = string(ev.Data[1])
wg.Add(1)
go func() {
var err error
defer wg.Done()
err = unix.Pipe(pfd[:])
if err != nil {
s.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to create event pipe for pty - %s", req.RemoteAddr, err.Error())
send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error
return
}
cmd, tty, err = connect_pty(s.pty_shell, s.pty_user)
if err != nil {
s.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to connect pty - %s", req.RemoteAddr, err.Error())
send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error - this will make websocket.MessageReceive to fail
unix.Close(pfd[0]); pfd[0] = -1
unix.Close(pfd[1]); pfd[1] = -1
return
}
err = send_ws_data_for_xterm(ws, "status", "opened")
if err != nil {
s.log.Write(pty.Id, LOG_ERROR, "[%s] Failed to write 'opened' event to websocket - %s", req.RemoteAddr, err.Error())
ws.Close() // dirty way to flag out the error
unix.Close(pfd[0]); pfd[0] = -1
unix.Close(pfd[1]); pfd[1] = -1
return
}
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Opened pty session", req.RemoteAddr)
out = tty
in = tty
conn_ready_chan <- true
}()
}
case "close":
if tty != nil {
tty.Close()
tty = nil
}
if pfd[1] >= 0 {
unix.Write(pfd[1], []byte{0})
}
break ws_recv_loop
case "iov":
if tty != nil {
var i int
for i, _ = range ev.Data {
in.Write([]byte(ev.Data[i]))
}
}
case "size":
if tty != nil && len(ev.Data) == 2 {
var rows int
var cols int
rows, _ = strconv.Atoi(ev.Data[0])
cols, _ = strconv.Atoi(ev.Data[1])
pts.Setsize(tty, &pts.Winsize{Rows: uint16(rows), Cols: uint16(cols)})
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Resized terminal to %d,%d", req.RemoteAddr, rows, cols)
// ignore error
}
}
}
}
}
if tty != nil {
err = send_ws_data_for_xterm(ws, "status", "closed")
if err != nil { goto done }
}
done:
conn_ready_chan <- false
ws.Close()
if cmd != nil {
// kill the child process underneath to close ptym(the master pty).
//cmd.Process.Signal(syscall.SIGTERM)
cmd.Process.Kill()
}
if tty != nil { tty.Close() }
if cmd != nil { cmd.Wait() }
wg.Wait()
// close the event pipe after all goroutines are over
if pfd[0] >= 0 { unix.Close(pfd[0]) }
if pfd[1] >= 0 { unix.Close(pfd[1]) }
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Ended pty session", req.RemoteAddr)
return http.StatusOK, err
}
// ------------------------------------------------------
func (rpty *server_rpty_ws) Identity() string {
return rpty.Id
}
func (rpty *server_rpty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
var s *Server
var req *http.Request
var token string
var cts *ServerConn
//var username string
//var password string
var rp *ServerRpty
var wg sync.WaitGroup
var err error
s = rpty.S
req = ws.Request()
token = req.FormValue("client-token")
if token == "" {
ws.Close()
return http.StatusBadRequest, fmt.Errorf("no client token specified")
}
cts = s.FindServerConnByClientToken(token)
if cts == nil {
ws.Close()
return http.StatusBadRequest, fmt.Errorf("invalid client token - %s", token)
}
ws_recv_loop:
for {
var msg []byte
err = websocket.Message.Receive(ws, &msg)
if err != nil { goto done }
if len(msg) > 0 {
var ev json_xterm_ws_event
err = json.Unmarshal(msg, &ev)
if err == nil {
switch ev.Type {
case "open":
if rp == nil && len(ev.Data) == 2 {
//username = string(ev.Data[0])
//password = string(ev.Data[1])
rp, err = cts.StartRpty(ws)
if err != nil {
s.log.Write(rpty.Id, LOG_ERROR, "[%s] Failed to connect pty - %s", req.RemoteAddr, err.Error())
send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error by making websocket.Message.Receive() fail
} else {
err = send_ws_data_for_xterm(ws, "status", "opened")
if err != nil {
s.log.Write(rpty.Id, LOG_ERROR, "[%s] Failed to write 'opened' event to websocket - %s", req.RemoteAddr, err.Error())
ws.Close() // dirty way to flag out the error
} else {
s.log.Write(rpty.Id, LOG_DEBUG, "[%s] Opened pty session", req.RemoteAddr)
}
}
}
case "close":
// just break out of the loop and let the remainder to close resources
break ws_recv_loop
case "iov":
var i int
for i, _ = range ev.Data {
cts.WriteRpty(ws, []byte(ev.Data[i]))
// ignore error for now
}
case "size":
if len(ev.Data) == 2 {
cts.WriteRptySize(ws, []byte(fmt.Sprintf("%s %s", ev.Data[0], ev.Data[1])))
s.log.Write(rpty.Id, LOG_DEBUG, "[%s] Requested to resize rpty terminal to %s,%s", req.RemoteAddr, ev.Data[0], ev.Data[1])
// ignore error
}
}
}
}
}
done:
cts.StopRpty(ws)
ws.Close() // don't care about multiple closes
wg.Wait()
s.log.Write(rpty.Id, LOG_DEBUG, "[%s] Ended rpty session for %s", req.RemoteAddr, token)
return http.StatusOK, err
}
// ------------------------------------------------------
func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var s *Server
var status_code int
var err error
s = pty.S
switch pty.file {
case "xterm.js":
status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_js)
case "xterm-addon-fit.js":
status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_addon_fit_js)
case "xterm.css":
status_code = WriteCssRespHeader(w, http.StatusOK)
w.Write(xterm_css)
case "xterm.html":
var tmpl *template.Template
tmpl = template.New("")
if s.xterm_html != "" {
_, err = tmpl.Parse(s.xterm_html)
} else {
_, err = tmpl.Parse(xterm_html)
}
if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError)
goto oops
} else {
status_code = WriteHtmlRespHeader(w, http.StatusOK)
tmpl.Execute(w,
&xterm_session_info{
Mode: pty.mode,
ConnId: "-1",
RouteId: "-1",
})
}
case "_forbidden":
status_code = WriteEmptyRespHeader(w, http.StatusForbidden)
case "_notfound":
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
default:
if strings.HasPrefix(pty.file, "_redir:") {
status_code = http.StatusMovedPermanently
w.Header().Set("Location", pty.file[7:])
w.WriteHeader(status_code)
} else {
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
}
}
//done:
return status_code, nil
oops:
return status_code, err
}

View File

@ -3,8 +3,8 @@ package hodu
import "bufio" import "bufio"
import "context" import "context"
import "crypto/tls" import "crypto/tls"
import _ "embed"
import "encoding/json" import "encoding/json"
import "errors"
import "fmt" import "fmt"
import "io" import "io"
import "net" import "net"
@ -21,32 +21,23 @@ import "golang.org/x/crypto/ssh"
import "golang.org/x/net/http/httpguts" import "golang.org/x/net/http/httpguts"
import "golang.org/x/net/websocket" import "golang.org/x/net/websocket"
//go:embed xterm.js type server_pxy struct {
var xterm_js []byte S *Server
//go:embed xterm-addon-fit.js Id string
var xterm_addon_fit_js []byte
//go:embed xterm.css
var xterm_css []byte
//go:embed xterm.html
var xterm_html string
type server_proxy struct {
s *Server
id string
} }
type server_proxy_http_main struct { type server_pxy_http_main struct {
server_proxy server_pxy
prefix string prefix string
} }
type server_proxy_xterm_file struct { type server_pxy_xterm_file struct {
server_proxy server_pxy
file string file string
} }
type server_proxy_http_wpx struct { type server_pxy_http_wpx struct {
server_proxy server_pxy
} }
// this is minimal information for wpx to work // this is minimal information for wpx to work
@ -184,17 +175,28 @@ func mutate_proxy_req_headers(req *http.Request, newreq *http.Request, path_pref
return upgrade_required return upgrade_required
} }
func (pxy *server_proxy) GetId() string { // ------------------------------------
return pxy.id
func (pxy *server_pxy) Identity() string {
return pxy.Id
}
func (pxy *server_pxy) Cors(req *http.Request) bool {
return false
}
func (pxy *server_pxy) Authenticate(req *http.Request) (int, string) {
return http.StatusOK, ""
} }
// ------------------------------------ // ------------------------------------
func prevent_follow_redirect (req *http.Request, via []*http.Request) error { func prevent_follow_redirect(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
func (pxy *server_proxy_http_main) get_route_proxy_info(req *http.Request, in_wpx_mode bool) (*ServerRouteProxyInfo, error) { func (pxy *server_pxy_http_main) get_route_proxy_info(req *http.Request, in_wpx_mode bool) (*ServerRouteProxyInfo, error) {
var s *Server
var conn_id string var conn_id string
var route_id string var route_id string
var r *ServerRoute var r *ServerRoute
@ -202,6 +204,8 @@ func (pxy *server_proxy_http_main) get_route_proxy_info(req *http.Request, in_wp
var path_prefix string var path_prefix string
var err error var err error
s = pxy.S
if in_wpx_mode { // for wpx if in_wpx_mode { // for wpx
conn_id = req.PathValue("port_id") conn_id = req.PathValue("port_id")
route_id = pxy.prefix // this is PORT_ID_MARKER route_id = pxy.prefix // this is PORT_ID_MARKER
@ -215,12 +219,12 @@ func (pxy *server_proxy_http_main) get_route_proxy_info(req *http.Request, in_wp
path_prefix = fmt.Sprintf("%s/%s/%s", pxy.prefix, conn_id, route_id) path_prefix = fmt.Sprintf("%s/%s/%s", pxy.prefix, conn_id, route_id)
} }
r, err = pxy.s.FindServerRouteByIdStr(conn_id, route_id) r, err = s.FindServerRouteByIdStr(conn_id, route_id)
if err != nil { if err != nil {
if !in_wpx_mode || pxy.s.wpx_foreign_port_proxy_maker == nil { return nil, err } if !in_wpx_mode || s.wpx_foreign_port_proxy_maker == nil { return nil, err }
// call this callback only in the wpx mode // call this callback only in the wpx mode
pi, err = pxy.s.wpx_foreign_port_proxy_maker("http", conn_id) pi, err = s.wpx_foreign_port_proxy_maker("http", conn_id)
if err != nil { return nil, err } if err != nil { return nil, err }
pi.IsForeign = true // just to ensure this pi.IsForeign = true // just to ensure this
} else { } else {
@ -234,7 +238,7 @@ func (pxy *server_proxy_http_main) get_route_proxy_info(req *http.Request, in_wp
return pi, nil return pi, nil
} }
func (pxy *server_proxy_http_main) serve_upgraded(w http.ResponseWriter, req *http.Request, proxy_res *http.Response) error { func (pxy *server_pxy_http_main) serve_upgraded(w http.ResponseWriter, req *http.Request, proxy_res *http.Response) error {
var err_chan chan error var err_chan chan error
var proxy_res_body io.ReadWriteCloser var proxy_res_body io.ReadWriteCloser
var rc *http.ResponseController var rc *http.ResponseController
@ -285,30 +289,36 @@ func (pxy *server_proxy_http_main) serve_upgraded(w http.ResponseWriter, req *ht
return err return err
} }
func (pxy *server_proxy_http_main) addr_to_transport (ctx context.Context, addr *net.TCPAddr) (*http.Transport, error) { func (pxy *server_pxy_http_main) addr_to_transport(ctx context.Context, addr *net.TCPAddr) (*http.Transport, error) {
var dialer *net.Dialer var dialer *net.Dialer
var waitctx context.Context var waitctx context.Context
var cancel_wait context.CancelFunc var cancel_wait context.CancelFunc
var conn net.Conn var conn net.Conn
var tls_config *tls.Config
var err error var err error
// establish the connection. // establish the connection.
dialer = &net.Dialer{} dialer = &net.Dialer{}
waitctx, cancel_wait = context.WithTimeout(ctx, 3 * time.Second) // TODO: make timeout configurable waitctx, cancel_wait = context.WithTimeout(ctx, 5 * time.Second) // TODO: make timeout configurable
conn, err = dialer.DialContext(waitctx, TcpAddrClass(addr), addr.String()) conn, err = dialer.DialContext(waitctx, TcpAddrClass(addr), addr.String())
cancel_wait() cancel_wait()
if err != nil { return nil, err } if err != nil { return nil, err }
if pxy.S.Cfg.PxyTargetTls != nil {
tls_config = pxy.S.Cfg.PxyTargetTls.Clone()
} else {
tls_config = &tls.Config{InsecureSkipVerify: true}
}
// create a transport that uses the connection // create a transport that uses the connection
return &http.Transport{ return &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return conn, nil return conn, nil
}, },
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // TODO: make this part configurable? TLSClientConfig: tls_config,
}, nil }, nil
} }
func (pxy *server_proxy_http_main) req_to_proxy_url (req *http.Request, r *ServerRouteProxyInfo) *url.URL { func (pxy *server_pxy_http_main) req_to_proxy_url(req *http.Request, r *ServerRouteProxyInfo) *url.URL {
var proxy_proto string var proxy_proto string
var proxy_url_path string var proxy_url_path string
@ -333,7 +343,7 @@ func (pxy *server_proxy_http_main) req_to_proxy_url (req *http.Request, r *Serve
} }
} }
func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) { func (pxy *server_pxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var s *Server var s *Server
var pi *ServerRouteProxyInfo var pi *ServerRouteProxyInfo
var status_code int var status_code int
@ -347,7 +357,7 @@ func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Re
var upgrade_required bool var upgrade_required bool
var err error var err error
s = pxy.s s = pxy.S
in_wpx_mode = (pxy.prefix == PORT_ID_MARKER) in_wpx_mode = (pxy.prefix == PORT_ID_MARKER)
pi, err = pxy.get_route_proxy_info(req, in_wpx_mode) pi, err = pxy.get_route_proxy_info(req, in_wpx_mode)
@ -364,16 +374,16 @@ func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Re
} }
*/ */
addr = svc_addr_to_dst_addr(pi.SvcAddr) addr = svc_addr_to_dst_addr(pi.SvcAddr)
transport, err = pxy.addr_to_transport(s.ctx, addr) transport, err = pxy.addr_to_transport(s.Ctx, addr)
if err != nil { if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusBadGateway) status_code = WriteEmptyRespHeader(w, http.StatusBadGateway)
goto oops goto oops
} }
proxy_url = pxy.req_to_proxy_url(req, pi) proxy_url = pxy.req_to_proxy_url(req, pi)
s.log.Write(pxy.id, LOG_INFO, "[%s] %s %s -> %+v", req.RemoteAddr, req.Method, req.URL.String(), proxy_url) s.log.Write(pxy.Id, LOG_INFO, "[%s] %s %s -> %+v", req.RemoteAddr, req.Method, req.RequestURI, proxy_url)
proxy_req, err = http.NewRequestWithContext(s.ctx, req.Method, proxy_url.String(), req.Body) proxy_req, err = http.NewRequestWithContext(s.Ctx, req.Method, proxy_url.String(), req.Body)
if err != nil { if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError) status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError)
goto oops goto oops
@ -397,7 +407,7 @@ func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Re
} else { } else {
status_code = resp.StatusCode status_code = resp.StatusCode
if upgrade_required && resp.StatusCode == http.StatusSwitchingProtocols { if upgrade_required && resp.StatusCode == http.StatusSwitchingProtocols {
s.log.Write(pxy.id, LOG_INFO, "[%s] %s %s %d", req.RemoteAddr, req.Method, req.URL.String(), status_code) s.log.Write(pxy.Id, LOG_INFO, "[%s] %s %s %d", req.RemoteAddr, req.Method, req.RequestURI, status_code)
err = pxy.serve_upgraded(w, req, resp) err = pxy.serve_upgraded(w, req, resp)
if err != nil { goto oops } if err != nil { goto oops }
return 0, nil// print the log mesage before calling serve_upgraded() and exit here return 0, nil// print the log mesage before calling serve_upgraded() and exit here
@ -422,7 +432,7 @@ func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Re
_, err = io.Copy(w, resp_body) _, err = io.Copy(w, resp_body)
if err != nil { if err != nil {
s.log.Write(pxy.id, LOG_WARN, "[%s] %s %s %s", req.RemoteAddr, req.Method, req.URL.String(), err.Error()) s.log.Write(pxy.Id, LOG_WARN, "[%s] %s %s %s", req.RemoteAddr, req.Method, req.RequestURI, err.Error())
} }
// TODO: handle trailers // TODO: handle trailers
@ -438,7 +448,7 @@ oops:
// ------------------------------------ // ------------------------------------
func (pxy *server_proxy_http_wpx) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) { func (pxy *server_pxy_http_wpx) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var status_code int var status_code int
// var err error // var err error
@ -454,24 +464,19 @@ func (pxy *server_proxy_http_wpx) ServeHTTP(w http.ResponseWriter, req *http.Req
} }
// ------------------------------------ // ------------------------------------
type server_proxy_xterm_session_info struct { func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
ConnId string
RouteId string
}
func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var s *Server var s *Server
var status_code int var status_code int
var err error var err error
s = pxy.s s = pxy.S
switch pxy.file { switch pxy.file {
case "xterm.js": case "xterm.js":
status_code = write_js_resp_header(w, http.StatusOK) status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_js) w.Write(xterm_js)
case "xterm-addon-fit.js": case "xterm-addon-fit.js":
status_code = write_js_resp_header(w, http.StatusOK) status_code = WriteJsRespHeader(w, http.StatusOK)
w.Write(xterm_addon_fit_js) w.Write(xterm_addon_fit_js)
case "xterm.css": case "xterm.css":
status_code = WriteCssRespHeader(w, http.StatusOK) status_code = WriteCssRespHeader(w, http.StatusOK)
@ -483,12 +488,12 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
// this endpoint is registered for /_ssh/{conn_id}/{route_id}/ under pxy. // this endpoint is registered for /_ssh/{conn_id}/{route_id}/ under pxy.
// and for /_ssh/{port_id} under wpx. // and for /_ssh/{port_id} under wpx.
if pxy.id == HS_ID_WPX { if pxy.Id == HS_ID_WPX {
conn_id = req.PathValue("port_id") conn_id = req.PathValue("port_id")
route_id = PORT_ID_MARKER route_id = PORT_ID_MARKER
_, err = s.FindServerRouteByIdStr(conn_id, route_id) _, err = s.FindServerRouteByIdStr(conn_id, route_id)
if err != nil && pxy.s.wpx_foreign_port_proxy_maker != nil { if err != nil && s.wpx_foreign_port_proxy_maker != nil {
_, err = pxy.s.wpx_foreign_port_proxy_maker("ssh", conn_id) _, err = s.wpx_foreign_port_proxy_maker("ssh", conn_id)
} }
} else { } else {
conn_id = req.PathValue("conn_id") conn_id = req.PathValue("conn_id")
@ -512,7 +517,8 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
} else { } else {
status_code = WriteHtmlRespHeader(w, http.StatusOK) status_code = WriteHtmlRespHeader(w, http.StatusOK)
tmpl.Execute(w, tmpl.Execute(w,
&server_proxy_xterm_session_info{ &xterm_session_info{
Mode: "ssh",
ConnId: conn_id, ConnId: conn_id,
RouteId: route_id, RouteId: route_id,
}) })
@ -528,8 +534,17 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
case "_forbidden": case "_forbidden":
status_code = WriteEmptyRespHeader(w, http.StatusForbidden) status_code = WriteEmptyRespHeader(w, http.StatusForbidden)
default: case "_notfound":
status_code = WriteEmptyRespHeader(w, http.StatusNotFound) status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
default:
if strings.HasPrefix(pxy.file, "_redir:") {
status_code = http.StatusMovedPermanently
w.Header().Set("Location", pxy.file[7:])
w.WriteHeader(status_code)
} else {
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
}
} }
//done: //done:
@ -540,31 +555,20 @@ oops:
} }
// ------------------------------------ // ------------------------------------
type server_pxy_ssh_ws struct {
type server_proxy_ssh_ws struct { S *Server
s *Server
ws *websocket.Conn ws *websocket.Conn
id string Id string
} }
type json_ssh_ws_event struct { func (pxy *server_pxy_ssh_ws) Identity() string {
Type string `json:"type"` return pxy.Id
Data []string `json:"data"`
} }
// TODO: put this task to sync group. // TODO: put this task to sync group.
// TODO: put the above proxy task to sync group too. // TODO: put the above proxy task to sync group too.
func (pxy *server_proxy_ssh_ws) send_ws_data(ws *websocket.Conn, type_val string, data string) error { func (pxy *server_pxy_ssh_ws) connect_ssh(ctx context.Context, username string, password string, r *ServerRoute) (*ssh.Client, *ssh.Session, io.Writer, io.Reader, error) {
var msg []byte
var err error
msg, err = json.Marshal(json_ssh_ws_event{Type: type_val, Data: []string{ data } })
if err == nil { err = websocket.Message.Send(ws, msg) }
return err
}
func (pxy *server_proxy_ssh_ws) connect_ssh (ctx context.Context, username string, password string, r *ServerRoute) ( *ssh.Client, *ssh.Session, io.Writer, io.Reader, error) {
var cc *ssh.ClientConfig var cc *ssh.ClientConfig
var addr *net.TCPAddr var addr *net.TCPAddr
var dialer *net.Dialer var dialer *net.Dialer
@ -578,6 +582,10 @@ func (pxy *server_proxy_ssh_ws) connect_ssh (ctx context.Context, username strin
var out io.Reader // ooutput from target var out io.Reader // ooutput from target
var err error var err error
// [NOTE]
// There is no authentication implemented for this websocket endpoint
// I suppose authentication should be done at the ssh layer.
// However, this can open doors to DoS attacks.
cc = &ssh.ClientConfig{ cc = &ssh.ClientConfig{
User: username, User: username,
Auth: []ssh.AuthMethod{ ssh.Password(password) }, Auth: []ssh.AuthMethod{ ssh.Password(password) },
@ -625,9 +633,10 @@ oops:
return nil, nil, nil, nil, err return nil, nil, nil, nil, err
} }
func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) { func (pxy *server_pxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
var s *Server var s *Server
var req *http.Request var req *http.Request
var port_id string
var conn_id string var conn_id string
var route_id string var route_id string
var r *ServerRoute var r *ServerRoute
@ -640,21 +649,28 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
var wg sync.WaitGroup var wg sync.WaitGroup
var conn_ready_chan chan bool var conn_ready_chan chan bool
var connect_ssh_ctx context.Context var connect_ssh_ctx context.Context
var connect_ssh_cancel context.CancelFunc var connect_ssh_cancel Atom[context.CancelFunc]
var err error var err error
s = pxy.s s = pxy.S
req = ws.Request() req = ws.Request()
conn_ready_chan = make(chan bool, 3) conn_ready_chan = make(chan bool, 3)
port_id = req.PathValue("port_id")
conn_id = req.PathValue("conn_id") conn_id = req.PathValue("conn_id")
route_id = req.PathValue("route_id") route_id = req.PathValue("route_id")
if port_id != "" && conn_id == "" && route_id == "" {
// called using the wpx endpoint. pxy.Id must be HS_ID_WPX
conn_id = port_id
route_id = PORT_ID_MARKER
}
r, err = s.FindServerRouteByIdStr(conn_id, route_id) r, err = s.FindServerRouteByIdStr(conn_id, route_id)
if err != nil && route_id == PORT_ID_MARKER && pxy.s.wpx_foreign_port_proxy_maker != nil { if err != nil && route_id == PORT_ID_MARKER && s.wpx_foreign_port_proxy_maker != nil {
var pi *ServerRouteProxyInfo var pi *ServerRouteProxyInfo
pi, err = pxy.s.wpx_foreign_port_proxy_maker("ssh", conn_id) pi, err = s.wpx_foreign_port_proxy_maker("ssh", conn_id)
if err != nil { if err != nil {
pxy.send_ws_data(ws, "error", err.Error()) send_ws_data_for_xterm(ws, "error", err.Error())
goto done goto done
} }
@ -666,7 +682,7 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
r = proxy_info_to_server_route(pi) r = proxy_info_to_server_route(pi)
} }
if err != nil { if err != nil {
pxy.send_ws_data(ws, "error", err.Error()) send_ws_data_for_xterm(ws, "error", err.Error())
goto done goto done
} }
@ -679,27 +695,27 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
conn_ready = <-conn_ready_chan conn_ready = <-conn_ready_chan
if conn_ready { // connected if conn_ready { // connected
var buf []byte var buf [2048]byte
var n int var n int
var err error var err error
s.stats.ssh_proxy_sessions.Add(1) s.stats.ssh_proxy_sessions.Add(1)
buf = make([]byte, 2048)
for { for {
n, err = out.Read(buf) n, err = out.Read(buf[:])
if err != nil {
if err != io.EOF {
s.log.Write(pxy.id, LOG_ERROR, "Read from SSH stdout error - %s", err.Error())
}
break
}
if n > 0 { if n > 0 {
err = pxy.send_ws_data(ws, "iov", string(buf[:n])) var err2 error
if err != nil { err2 = send_ws_data_for_xterm(ws, "iov", string(buf[:n]))
s.log.Write(pxy.id, LOG_ERROR, "Failed to send to websocket - %s", err.Error()) if err2 != nil {
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to send to websocket - %s", req.RemoteAddr, err2.Error())
break break
} }
} }
if err != nil {
if !errors.Is(err, io.EOF) {
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to read from SSH stdout - %s", req.RemoteAddr, err.Error())
}
break
}
} }
s.stats.ssh_proxy_sessions.Add(-1) s.stats.ssh_proxy_sessions.Add(-1)
} }
@ -712,16 +728,19 @@ ws_recv_loop:
if err != nil { goto done } if err != nil { goto done }
if len(msg) > 0 { if len(msg) > 0 {
var ev json_ssh_ws_event var ev json_xterm_ws_event
err = json.Unmarshal(msg, &ev) err = json.Unmarshal(msg, &ev)
if err == nil { if err == nil {
switch ev.Type { switch ev.Type {
case "open": case "open":
if sess == nil && len(ev.Data) == 2 { if sess == nil && len(ev.Data) == 2 {
var cancel context.CancelFunc
username = string(ev.Data[0]) username = string(ev.Data[0])
password = string(ev.Data[1]) password = string(ev.Data[1])
connect_ssh_ctx, connect_ssh_cancel = context.WithTimeout(req.Context(), 10 * time.Second) // TODO: configurable timeout connect_ssh_ctx, cancel = context.WithTimeout(req.Context(), 10 * time.Second) // TODO: configurable timeout
connect_ssh_cancel.Set(cancel)
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -730,26 +749,27 @@ ws_recv_loop:
defer wg.Done() defer wg.Done()
c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r) c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r)
if err != nil { if err != nil {
s.log.Write(pxy.id, LOG_ERROR, "failed to connect ssh - %s", err.Error()) s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to connect ssh - %s", req.RemoteAddr, err.Error())
pxy.send_ws_data(ws, "error", err.Error()) send_ws_data_for_xterm(ws, "error", err.Error())
ws.Close() // dirty way to flag out the error ws.Close() // dirty way to flag out the error
} else { } else {
err = pxy.send_ws_data(ws, "status", "opened") err = send_ws_data_for_xterm(ws, "status", "opened")
if err != nil { if err != nil {
s.log.Write(pxy.id, LOG_ERROR, "Failed to write opened event to websocket - %s", err.Error()) s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to write opened event to websocket - %s", req.RemoteAddr, err.Error())
ws.Close() // dirty way to flag out the error ws.Close() // dirty way to flag out the error
} else { } else {
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Opened SSH session", req.RemoteAddr)
conn_ready_chan <- true conn_ready_chan <- true
} }
} }
connect_ssh_cancel() (connect_ssh_cancel.Get())()
connect_ssh_cancel = nil connect_ssh_cancel.Set(nil) // @@@ use atomic
}() }()
} }
case "close": case "close":
var cancel context.CancelFunc var cancel context.CancelFunc
cancel = connect_ssh_cancel // is it a good way to avoid mutex? cancel = connect_ssh_cancel.Get() // is it a good way to avoid mutex against Set() marked with @@@ above?
if cancel != nil { cancel() } if cancel != nil { cancel() }
break ws_recv_loop break ws_recv_loop
@ -768,7 +788,7 @@ ws_recv_loop:
rows, _ = strconv.Atoi(ev.Data[0]) rows, _ = strconv.Atoi(ev.Data[0])
cols, _ = strconv.Atoi(ev.Data[1]) cols, _ = strconv.Atoi(ev.Data[1])
sess.WindowChange(rows, cols) sess.WindowChange(rows, cols)
s.log.Write(pxy.id, LOG_DEBUG, "Resized terminal to %d,%d", rows, cols) s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Resized terminal to %d,%d", req.RemoteAddr, rows, cols)
// ignore error // ignore error
} }
} }
@ -777,7 +797,7 @@ ws_recv_loop:
} }
if sess != nil { if sess != nil {
err = pxy.send_ws_data(ws, "status", "closed") err = send_ws_data_for_xterm(ws, "status", "closed")
if err != nil { goto done } if err != nil { goto done }
} }
@ -787,9 +807,7 @@ done:
if sess != nil { sess.Close() } if sess != nil { sess.Close() }
if c != nil { c.Close() } if c != nil { c.Close() }
wg.Wait() wg.Wait()
if err != nil { s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Ended SSH Session", req.RemoteAddr)
s.log.Write(pxy.id, LOG_ERROR, "[%s] %s %s - %s", req.RemoteAddr, req.Method, req.URL.String(), err.Error())
} else { return http.StatusOK, err
s.log.Write(pxy.id, LOG_DEBUG, "[%s] %s %s - ended", req.RemoteAddr, req.Method, req.URL.String())
}
} }

330
server-rpx.go Normal file
View File

@ -0,0 +1,330 @@
package hodu
import "bufio"
import "bytes"
import "errors"
import "fmt"
import "io"
import "net"
import "net/http"
import "strconv"
import "strings"
import "sync"
type server_rpx struct {
S *Server
Id string
}
// ------------------------------------
func (rpx *server_rpx) Identity() string {
return rpx.Id
}
func (rpx *server_rpx) Cors(req *http.Request) bool {
return false
}
func (rpx *server_rpx) Authenticate(req *http.Request) (int, string) {
return http.StatusOK, ""
}
func (rpx *server_rpx) get_client_token(req *http.Request) string {
var val string
// TODO: enhance this client token extraction logic with some expression language?
val = req.Header.Get(rpx.S.Cfg.RpxClientTokenAttrName)
if val == "" { val = req.Host }
if rpx.S.Cfg.RpxClientTokenRegex != nil {
val = get_regex_submatch(rpx.S.Cfg.RpxClientTokenRegex, val, rpx.S.Cfg.RpxClientTokenSubmatchIndex)
}
return val
}
func (rpx* server_rpx) handle_header_data(rpx_id uint64, data []byte, w http.ResponseWriter) (int, error) {
var sc *bufio.Scanner
var line string
var flds []string
var status_code int
var err error
sc = bufio.NewScanner(bytes.NewReader(data))
sc.Scan()
line = sc.Text()
flds = strings.Fields(line)
if (len(flds) < 2) { // i care about the status code..
return http.StatusBadGateway, fmt.Errorf("invalid response status for rpx(%d) - %s", rpx_id, line)
}
status_code, err = strconv.Atoi(flds[1])
if err != nil {
return http.StatusBadGateway, fmt.Errorf("invalid response code for rpx(%d) - %s", rpx_id, err.Error())
}
for sc.Scan() {
line = sc.Text()
if line == "" { break }
flds = strings.SplitN(line, ":", 2)
if len(flds) == 2 {
w.Header().Add(strings.TrimSpace(flds[0]), strings.TrimSpace(flds[1]))
}
}
err = sc.Err()
if err != nil {
return http.StatusBadGateway, fmt.Errorf("failed to parse response for rpx(%d) - %s", rpx_id, err.Error())
}
w.WriteHeader(status_code)
return status_code, nil
}
func (rpx *server_rpx) handle_response(srpx *ServerRpx, req *http.Request, w http.ResponseWriter, ws_upgrade bool, wg *sync.WaitGroup) {
var start_resp []byte
var status_code int
var buf [4096]byte
var n int
var wr io.Writer
var wrote_br_chan bool
var err error
defer wg.Done()
select {
case start_resp = <- srpx.start_chan:
// received the header. ready to proceed to the body
// do nothing. just continue
status_code, err = rpx.handle_header_data(srpx.id, start_resp, w)
if err != nil { goto done }
case <- srpx.done_chan:
err = fmt.Errorf("rpx(%d) terminated before receiving header", srpx.id)
status_code = http.StatusBadGateway
goto done
case <- req.Context().Done():
err = fmt.Errorf("rpx(%d) terminated before receiving header - %s", srpx.id, req.Context().Err().Error())
status_code = http.StatusBadGateway
goto done
// no default. block
}
if ws_upgrade && status_code == http.StatusSwitchingProtocols {
var hijk http.Hijacker
var conn net.Conn
var ok bool
hijk, ok = w.(http.Hijacker)
if !ok {
err = fmt.Errorf("failed to upgrade rpx(%d) - not a hijacker", srpx.id)
status_code = http.StatusInternalServerError
goto done
}
conn, _, err = hijk.Hijack()
if err != nil {
err = fmt.Errorf("failed to upgrade rpx(%d) - %s", srpx.id, err.Error())
status_code = http.StatusInternalServerError
goto done
}
// websocket upgrade is successful
srpx.br = conn
srpx.br_chan <- true // inform another goroutine that the protocol switching is completed.
wrote_br_chan = true
wr = conn
} else {
if ws_upgrade {
srpx.br_chan <- false
wrote_br_chan = true
} // indicate upgrade failure
wr = w
}
for {
n, err = srpx.pr.Read(buf[:])
if n > 0 {
var err2 error
_, err2 = wr.Write(buf[:n])
if err2 != nil {
err = err2
status_code = http.StatusInternalServerError
break
}
}
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
} else {
status_code = http.StatusInternalServerError
}
break
}
}
done:
// just send another in case the code got jump into this part for an error
// may not be consumed but the channel is large enough for redundant data
srpx.resp_status_code = status_code
srpx.resp_error = err
if ws_upgrade && !wrote_br_chan {
srpx.br_chan <- false
}
}
func (rpx *server_rpx) alloc_server_rpx(cts *ServerConn, req *http.Request) (*ServerRpx, error) {
var srpx *ServerRpx
var start_id uint64
var assigned_id uint64
var ok bool
cts.rpx_mtx.Lock()
start_id = cts.rpx_next_id
for {
_, ok = cts.rpx_map[cts.rpx_next_id]
if !ok {
assigned_id = cts.rpx_next_id
cts.rpx_next_id++
if cts.rpx_next_id == 0 { cts.rpx_next_id++ }
break
}
cts.rpx_next_id++
if cts.rpx_next_id == 0 { cts.rpx_next_id++ }
if cts.rpx_next_id == start_id {
// unlikely to happen but it cycled through the whole range.
cts.rpx_mtx.Unlock()
return nil, fmt.Errorf("failed to assign id")
}
}
srpx = &ServerRpx{
id: assigned_id,
start_chan: make(chan []byte, 5),
done_chan: make(chan bool, 5),
br_chan: make(chan bool, 5),
}
srpx.br = req.Body
srpx.pr, srpx.pw = io.Pipe()
cts.rpx_map[assigned_id] = srpx
cts.rpx_mtx.Unlock()
cts.S.stats.rpx_sessions.Add(1)
return srpx, nil
}
func (rpx *server_rpx) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
var s *Server
var client_token string
var start_sent bool
var cts *ServerConn
var status_code int
var srpx *ServerRpx
var ws_upgrade bool
var buf [4096]byte
var wg sync.WaitGroup
var err error
s = rpx.S
client_token = rpx.get_client_token(req)
cts = s.FindServerConnByClientToken(client_token)
if cts == nil {
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
err = fmt.Errorf("unknown client token - %s", client_token)
goto oops
}
srpx, err = rpx.alloc_server_rpx(cts, req)
if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusServiceUnavailable)
err = fmt.Errorf("unable to allocate rpx - %s", err.Error())
goto oops
}
// arrange to clear the rpx_map entry when this function exits
defer func() {
cts.rpx_mtx.Lock()
delete(cts.rpx_map, srpx.id)
cts.rpx_mtx.Unlock()
cts.S.stats.rpx_sessions.Add(-1)
}()
ws_upgrade = strings.EqualFold(req.Header.Get("Upgrade"), "websocket") && strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade");
if ws_upgrade && req.ContentLength > 0 {
// while other webservers are ok with upgrade request with body payload,
// this program rejects such a request for impelementation limitation as
// it's not dealing with a raw byte but is using the standard web server handler.
status_code = WriteEmptyRespHeader(w, http.StatusBadRequest)
err = fmt.Errorf("failed to assign id")
goto oops
}
err = cts.pss.Send(MakeRpxStartPacket(srpx.id, get_http_req_line_and_headers(req, true)))
if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusBadGateway)
goto oops
}
start_sent = true
wg.Add(1)
go rpx.handle_response(srpx, req, w, ws_upgrade, &wg)
if ws_upgrade {
// wait until the protocol switching is done in rpx.handle_response()
var upgraded bool
upgraded = <- srpx.br_chan
if upgraded {
// arrange to close the hijacked connection inside rpx.handle_response()
defer srpx.br.Close()
}
}
for {
var n int
n, err = srpx.br.Read(buf[:])
if n > 0 {
var err2 error
err2 = cts.pss.Send(MakeRpxDataPacket(srpx.id, buf[:n]))
if err2 != nil {
status_code = WriteEmptyRespHeader(w, http.StatusBadGateway)
goto oops
}
}
if err != nil {
if errors.Is(err, io.EOF) {
err = cts.pss.Send(MakeRpxEofPacket(srpx.id))
if err != nil {
status_code = WriteEmptyRespHeader(w, http.StatusBadGateway)
goto oops
}
break
}
status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError)
goto oops
}
}
wg.Wait()
if srpx.resp_error != nil {
status_code = WriteEmptyRespHeader(w, srpx.resp_status_code)
err = srpx.resp_error
goto oops
}
select {
case <- srpx.done_chan:
// anything to do?
case <- req.Context().Done():
// anything to do?
// no default. block
}
cts.pss.Send(MakeRpxStopPacket(srpx.id))
return srpx.resp_status_code, nil
oops:
if srpx != nil && start_sent { cts.pss.Send(MakeRpxStopPacket(srpx.id)) }
return status_code, err
}

1906
server.go

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terminal</title> <title>Terminal</title>
<link rel="stylesheet" href="/_ssh/xterm.css" /> <link rel="stylesheet" href="xterm.css" />
<style> <style>
body { body {
margin: 0; margin: 0;
@ -86,10 +86,11 @@
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script src="/_ssh/xterm.js"></script> <script src="xterm.js"></script>
<script src="/_ssh/xterm-addon-fit.js"></script> <script src="xterm-addon-fit.js"></script>
<script> <script>
const xt_mode = '{{ .Mode }}';
const conn_id = '{{ .ConnId }}'; const conn_id = '{{ .ConnId }}';
const route_id = '{{ .RouteId }}'; const route_id = '{{ .RouteId }}';
@ -99,12 +100,28 @@ window.onload = function(event) {
const terminal_status = document.getElementById('terminal-status'); const terminal_status = document.getElementById('terminal-status');
const terminal_errmsg = document.getElementById('terminal-errmsg'); const terminal_errmsg = document.getElementById('terminal-errmsg');
const terminal_view_container = document.getElementById('terminal-view-container'); const terminal_view_container = document.getElementById('terminal-view-container');
const terminal_connect = document.getElementById('terminal-connect');
const terminal_disconnect = document.getElementById('terminal-disconnect'); const terminal_disconnect = document.getElementById('terminal-disconnect');
const login_container = document.getElementById('login-container'); const login_container = document.getElementById('login-container');
const login_form_title = document.getElementById('login-form-title'); const login_form_title = document.getElementById('login-form-title');
const login_form = document.getElementById('login-form'); const login_form = document.getElementById('login-form');
const login_ssh_part = document.getElementById('login-ssh-part');
const login_pty_part = document.getElementById('login-pty-part');
const username_field = document.getElementById('username'); const username_field = document.getElementById('username');
const password_field= document.getElementById('password'); const password_field= document.getElementById('password');
const qparams = new URLSearchParams(window.location.search);
if (xt_mode == 'ssh') {
login_ssh_part.style.display = 'block';
username_field.disabled = false;
password_field.disabled = false;
login_pty_part.style.display = 'none';
} else {
login_ssh_part.style.display = 'none';
username_field.disabled = true;
password_field.disabled = true;
login_pty_part.style.display = 'block';
}
const term = new window.Terminal({ const term = new window.Terminal({
lineHeight: 1.2, lineHeight: 1.2,
@ -133,11 +150,19 @@ window.onload = function(event) {
let fetch_session_info = async function() { let fetch_session_info = async function() {
let url = window.location.protocol + '//' + window.location.host; let url = window.location.protocol + '//' + window.location.host;
url += `/_ssh/server-conns/${conn_id}/routes/${route_id}`; let pathname = window.location.pathname;
//pathname = pathname.replace(/\/$/, '');
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
url += pathname + `/session-info`;
try { try {
const resp = await fetch(url); const resp = await fetch(url);
if (!resp.ok) throw new Error(`HTTP error in getting route(${conn_id},${route_id}) info - status ${resp.status}`); if (!resp.ok) {
if (xt_mode == 'ssh')
throw new Error(`HTTP error in getting route(${conn_id},${route_id}) information - status ${resp.status}`);
else
throw new Error(`HTTP error in getting session information - status ${resp.status}`);
}
const route = await resp.json() const route = await resp.json()
if ('client-peer-name' in route) { if ('client-peer-name' in route) {
set_terminal_target(route['client-peer-name']) // change to the name set_terminal_target(route['client-peer-name']) // change to the name
@ -151,11 +176,15 @@ window.onload = function(event) {
} }
let toggle_login_form = function(visible) { let toggle_login_form = function(visible) {
if (visible) fetch_session_info(); if (visible && xt_mode == 'ssh') fetch_session_info();
login_container.style.visibility = (visible? 'visible': 'hidden'); login_container.style.visibility = (visible? 'visible': 'hidden');
terminal_disconnect.style.visibility = (visible? 'hidden': 'visible'); terminal_disconnect.style.visibility = (visible? 'hidden': 'visible');
if (visible) username_field.focus(); if (visible) {
else term.focus(); if (xt_mode == 'ssh') username_field.focus();
else terminal_connect.focus();
} else {
term.focus();
}
} }
toggle_login_form(true); toggle_login_form(true);
@ -166,12 +195,27 @@ window.onload = function(event) {
event.preventDefault(); event.preventDefault();
toggle_login_form(false) toggle_login_form(false)
const username = username_field.value.trim(); let username = '';
const password = password_field.value.trim(); let password = '';
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; if (xt_mode == 'ssh') {
let url = prefix + window.location.host+ `/_ssh-ws/${conn_id}/${route_id}`; username = username_field.value.trim();
const socket = new WebSocket(url) password = password_field.value.trim();
}
const prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
let pathname = window.location.pathname;
//pathname = pathname.replace(/\/$/, '');
pathname = pathname.substring(0, pathname.lastIndexOf('/'));
let url = prefix + window.location.host + pathname + '/ws';
if (xt_mode == 'rpty') {
// when accessing rpty, the server requires a client token
let client_token = qparams.get('client-token');
if (client_token != null && client_token != '') url += '?client-token=' + client_token;
}
const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer'; socket.binaryType = 'arraybuffer';
set_terminal_status('Connecting...', ''); set_terminal_status('Connecting...', '');
@ -217,7 +261,8 @@ window.onload = function(event) {
}; };
socket.onerror = function(event) { socket.onerror = function(event) {
set_terminal_status('Disconnected', event); //set_terminal_status('Disconnected', event);
set_terminal_status('Disconnected', '');
toggle_login_form(true) toggle_login_form(true)
window.onresize = adjust_terminal_size_unconnected; window.onresize = adjust_terminal_size_unconnected;
}; };
@ -249,15 +294,21 @@ window.onload = function(event) {
<div id="login-form-container"> <div id="login-form-container">
<div id="login-form-title"></div> <div id="login-form-title"></div>
<form id="login-form"> <form id="login-form">
<div id="login-ssh-part" style="display: none;">
<label> <label>
Username: <input type="text" id="username" required /> Username: <input type="text" id="username" disabled required />
</label> </label>
<br /><br /> <br /><br />
<label> <label>
Password: <input type="password" id="password" required /> Password: <input type="password" id="password" disabled required />
</label> </label>
<br /><br /> </div>
<button type="submit">Connect</button> <div id="login-pty-part" style="display: none;">
Click Connect below to start a new terminal session
</div>
<div id="login-submit-part" style="padding-top: 1em;">
<button type="submit" id="terminal-connect">Connect</button>
</div>
</form> </form>
</div> </div>
</div> </div>