Compare commits
119 Commits
e1eb346228
...
main
Author | SHA1 | Date | |
---|---|---|---|
437ab8ccef | |||
714748d8e2 | |||
f5a95505a9 | |||
98966f85f6 | |||
c8cb71cf95 | |||
e2a3180ec7 | |||
42ceb5f3fa | |||
9f8d3e2696 | |||
7ec1387132 | |||
7d3ce7147a | |||
0d76146bc3 | |||
1dcb1605b7 | |||
6078a41504 | |||
0696f4f560 | |||
c001458b24 | |||
10c139e837 | |||
31a4223aab | |||
41cd725c1c | |||
6200bc5460 | |||
7fb4fbaae2 | |||
d818acc53d | |||
05cb0823b4 | |||
d0f1663bf3 | |||
3fd91b2c45 | |||
5767beb9af | |||
6e670b4924 | |||
8331fdc1a2 | |||
d092540f08 | |||
7835696166 | |||
c5bac71eaf | |||
9addb5d35f | |||
e12cd28413 | |||
6baf3b2b53 | |||
01eb2edd6e | |||
cea0efcb18 | |||
db9fc1f4d9 | |||
54e9e208f4 | |||
f06433d431 | |||
8318643735 | |||
deb6f7b05a | |||
dee3711dd4 | |||
be864129dc | |||
e1c288f17f | |||
a1f8d4cf22 | |||
9c3a4d0c17 | |||
b41df682e1 | |||
918b887517 | |||
76cba687ed | |||
fd28add458 | |||
0cfd241e00 | |||
5dbe9cd34e | |||
494f11836c | |||
41503373d3 | |||
e01a6b347c | |||
8cde9f08d4 | |||
9865914436 | |||
5fa6cd466b | |||
3714138656 | |||
f1f74ed48d | |||
cd32380425 | |||
4d3fb7db65 | |||
8105545e98 | |||
1e6fbed19d | |||
b398816c96 | |||
09593fd678 | |||
befe65b486 | |||
030d62af12 | |||
ae13d0c4ed | |||
7b1d383813 | |||
6e9887f726 | |||
ecc1d4580f | |||
e56c45b3bf | |||
b6fb296608 | |||
bec93289f5 | |||
04e2de609e | |||
71a42af593 | |||
5df95159a3 | |||
2d63e81e62 | |||
97885bcae1 | |||
5c2695e46b | |||
75f72e7c88 | |||
7363986737 | |||
429bb6cd63 | |||
d9aaa0a0ab | |||
7a6b820b92 | |||
c7b7bfd25f | |||
1c49023c37 | |||
b5c1ae2a73 | |||
2b3a841299 | |||
81f7bb0c0d | |||
a0efb55c3e | |||
be7f4f4da5 | |||
f2536a0acc | |||
cb18a44cfa | |||
fb465133b9 | |||
d5108e9859 | |||
3dc5d9c91e | |||
ec51c101ec | |||
ef3e80efb8 | |||
1bc8406907 | |||
0fb57cb77b | |||
16327fc576 | |||
148dfbcfe1 | |||
8bee855aa8 | |||
b7992a0bb7 | |||
2fa5817e88 | |||
a97be385ec | |||
d3afe29d5a | |||
2655da937f | |||
c5f63328b2 | |||
810356efe5 | |||
90305e3eed | |||
cfc0db4b54 | |||
0809f9bedc | |||
e2f1d58c5e | |||
edda6d169b | |||
9118acb268 | |||
369a41eeb2 | |||
c237b8a842 |
33
Makefile
33
Makefile
@ -9,17 +9,26 @@ NAME=hodu
|
||||
VERSION=1.0.0
|
||||
|
||||
SRCS=\
|
||||
atom.go \
|
||||
bulletin.go \
|
||||
client.go \
|
||||
client-ctl.go \
|
||||
client-metrics.go \
|
||||
client-peer.go \
|
||||
client-pty.go \
|
||||
hodu.go \
|
||||
hodu.pb.go \
|
||||
hodu_grpc.pb.go \
|
||||
jwt.go \
|
||||
packet.go \
|
||||
pty.go \
|
||||
server.go \
|
||||
server-ctl.go \
|
||||
server-metrics.go \
|
||||
server-peer.go \
|
||||
server-proxy.go \
|
||||
server-pty.go \
|
||||
server-pxy.go \
|
||||
server-rpx.go \
|
||||
system.go \
|
||||
transform.go \
|
||||
|
||||
@ -30,6 +39,7 @@ DATA = \
|
||||
xterm.html
|
||||
|
||||
CMD_DATA=\
|
||||
cmd/rsa.key \
|
||||
cmd/tls.crt \
|
||||
cmd/tls.key
|
||||
|
||||
@ -41,13 +51,19 @@ CMD_SRCS=\
|
||||
all: $(NAME)
|
||||
|
||||
$(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=1 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)' -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:
|
||||
go clean -x -i
|
||||
rm -f $(NAME)
|
||||
rm -f $(NAME) $(NAME).debug
|
||||
|
||||
check:
|
||||
go test -x
|
||||
|
||||
hodu.pb.go: hodu.proto
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
@ -70,9 +86,12 @@ xterm.css:
|
||||
sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@"
|
||||
|
||||
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:
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
## 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 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
|
||||
```
|
||||
@ -29,13 +29,13 @@
|
||||
"client-peer-addr": "192.168.1.104:22",
|
||||
"client-peer-name": "Star gate",
|
||||
"server-peer-option": "tcp4 ssh",
|
||||
"server-peer-service-addr": "0.0.0.0:0",
|
||||
"server-peer-service-net": "",
|
||||
"server-peer-svc-addr": "0.0.0.0:0",
|
||||
"server-peer-svc-net": "",
|
||||
"lifetime": "0"
|
||||
}
|
||||
```
|
||||
|
||||
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
21
atom.go
Normal 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
282
bulletin.go
Normal 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
139
bulletin_test.go
Normal 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) }
|
||||
}
|
932
client-ctl.go
932
client-ctl.go
File diff suppressed because it is too large
Load Diff
126
client-metrics.go
Normal file
126
client-metrics.go
Normal 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()),
|
||||
)
|
||||
}
|
@ -26,34 +26,49 @@ func (cpc *ClientPeerConn) RunTask(wg *sync.WaitGroup) error {
|
||||
var n int
|
||||
|
||||
defer wg.Done()
|
||||
cpc.route.cts.C.FirePeerEvent(CLIENT_EVENT_PEER_STARTED, cpc)
|
||||
|
||||
for {
|
||||
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 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",
|
||||
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 {
|
||||
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",
|
||||
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
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
|
299
client-pty.go
Normal file
299
client-pty.go
Normal 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
|
||||
}
|
24
client_test.go
Normal file
24
client_test.go
Normal 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") }
|
||||
}
|
200
cmd/config.go
200
cmd/config.go
@ -1,16 +1,20 @@
|
||||
package main
|
||||
|
||||
import "crypto/rsa"
|
||||
import "crypto/tls"
|
||||
import "crypto/x509"
|
||||
import "errors"
|
||||
import "encoding/base64"
|
||||
import "encoding/pem"
|
||||
import "fmt"
|
||||
import "hodu"
|
||||
import "io"
|
||||
import "io/ioutil"
|
||||
import "net/netip"
|
||||
import "os"
|
||||
import "strings"
|
||||
import "time"
|
||||
|
||||
import "gopkg.in/yaml.v3"
|
||||
//import "gopkg.in/yaml.v3"
|
||||
import yaml "github.com/goccy/go-yaml"
|
||||
|
||||
|
||||
type ServerTLSConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
@ -42,9 +46,37 @@ type ClientTLSConfig struct {
|
||||
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 {
|
||||
Prefix string `yaml:"prefix"` // url prefix for control channel endpoints
|
||||
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 {
|
||||
@ -72,6 +104,8 @@ type ServerAppConfig struct {
|
||||
LogRotate int `yaml:"log-rotate"`
|
||||
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -83,6 +117,11 @@ type ClientAppConfig struct {
|
||||
MaxPeers int `yaml:"max-peer-conns"` // maximum number of connections from peers
|
||||
MaxRpcConns int `yaml:"max-rpc-conns"` // maximum number of rpc connections
|
||||
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 {
|
||||
@ -93,9 +132,18 @@ type ServerConfig struct {
|
||||
TLS ServerTLSConfig `yaml:"tls"`
|
||||
} `yaml:"ctl"`
|
||||
|
||||
RPX struct {
|
||||
Service RPXServiceConfig `yaml:"service"`
|
||||
TLS ServerTLSConfig `yaml:"tls"`
|
||||
ClientToken RPXClientTokenConfig `yaml:"client-token"`
|
||||
} `yaml:"rpx"`
|
||||
|
||||
PXY struct {
|
||||
Service PXYServiceConfig `yaml:"service"`
|
||||
TLS ServerTLSConfig `yaml:"tls"`
|
||||
Target struct {
|
||||
TLS ClientTLSConfig `yaml:"tls"`
|
||||
} `yaml:"target"`
|
||||
} `yaml:"pxy"`
|
||||
|
||||
WPX struct {
|
||||
@ -120,48 +168,40 @@ type ClientConfig struct {
|
||||
Endpoint RPCEndpointConfig `yaml:"endpoint"`
|
||||
TLS ClientTLSConfig `yaml:"tls"`
|
||||
} `yaml:"rpc"`
|
||||
RPX struct {
|
||||
Target struct {
|
||||
Addr string `yaml:"address"`
|
||||
TLS ClientTLSConfig `yaml:"tls"`
|
||||
} `yaml:"target"`
|
||||
}
|
||||
}
|
||||
|
||||
func load_server_config(cfgfile string) (*ServerConfig, error) {
|
||||
var cfg ServerConfig
|
||||
func load_server_config_to(cfgfile string, cfg *ServerConfig) error {
|
||||
var f *os.File
|
||||
var yd *yaml.Decoder
|
||||
var err error
|
||||
|
||||
f, err = os.Open(cfgfile)
|
||||
if err != nil && errors.Is(err, io.EOF) {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
yd = yaml.NewDecoder(f)
|
||||
err = yd.Decode(&cfg)
|
||||
yd = yaml.NewDecoder(f, yaml.AllowDuplicateMapKey(), yaml.DisallowUnknownField())
|
||||
err = yd.Decode(cfg)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return err
|
||||
}
|
||||
|
||||
func load_client_config(cfgfile string) (*ClientConfig, error) {
|
||||
var cfg ClientConfig
|
||||
func load_client_config_to(cfgfile string, cfg *ClientConfig) error {
|
||||
var f *os.File
|
||||
var yd *yaml.Decoder
|
||||
var err error
|
||||
|
||||
f, err = os.Open(cfgfile)
|
||||
if err != nil && errors.Is(err, io.EOF) {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil { return err }
|
||||
|
||||
yd = yaml.NewDecoder(f)
|
||||
err = yd.Decode(&cfg)
|
||||
yd = yaml.NewDecoder(f, yaml.AllowDuplicateMapKey(), yaml.DisallowUnknownField())
|
||||
err = yd.Decode(cfg)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
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()
|
||||
@ -248,7 +288,7 @@ func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
|
||||
}
|
||||
} else if cfg.ClientCACertFile != "" {
|
||||
var text []byte
|
||||
text, err = ioutil.ReadFile(cfg.ClientCACertFile)
|
||||
text, err = os.ReadFile(cfg.ClientCACertFile)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
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()
|
||||
@ -306,7 +346,7 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
|
||||
}
|
||||
} else if cfg.ServerCACertFile != "" {
|
||||
var text []byte
|
||||
text, err = ioutil.ReadFile(cfg.ServerCACertFile)
|
||||
text, err = os.ReadFile(cfg.ServerCACertFile)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
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
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import "runtime"
|
||||
import "strings"
|
||||
import "sync"
|
||||
import "sync/atomic"
|
||||
import "syscall"
|
||||
import "time"
|
||||
|
||||
type app_logger_msg_t struct {
|
||||
@ -28,16 +29,44 @@ type AppLogger struct {
|
||||
msg_chan chan app_logger_msg_t
|
||||
wg sync.WaitGroup
|
||||
|
||||
use_color 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 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{
|
||||
id: id,
|
||||
out: w,
|
||||
mask: mask,
|
||||
msg_chan: make(chan app_logger_msg_t, 256),
|
||||
use_color: use_color,
|
||||
}
|
||||
l.closed.Store(false)
|
||||
l.wg.Add(1)
|
||||
@ -45,7 +74,7 @@ func NewAppLogger (id string, w io.Writer, mask hodu.LogMask) *AppLogger {
|
||||
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 f *os.File
|
||||
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_rotate: rotate,
|
||||
msg_chan: make(chan app_logger_msg_t, 256),
|
||||
use_color: _is_ansi_tty(f.Fd()),
|
||||
}
|
||||
l.closed.Store(false)
|
||||
l.wg.Add(1)
|
||||
@ -121,7 +151,6 @@ main_loop:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (l *AppLogger) Write(id string, level hodu.LogLevel, fmtstr string, args ...interface{}) {
|
||||
if l.mask & hodu.LogMask(level) == 0 { return }
|
||||
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()
|
||||
|
||||
_, off_s = now.Zone()
|
||||
off_m = off_s / 60;
|
||||
off_h = off_m / 60;
|
||||
off_m = off_m % 60;
|
||||
if off_m < 0 { off_m = -off_m; }
|
||||
off_m = off_s / 60
|
||||
off_h = off_m / 60
|
||||
off_m = off_m % 60
|
||||
if off_m < 0 { off_m = -off_m }
|
||||
|
||||
sb.WriteString(
|
||||
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(": ")
|
||||
msg = fmt.Sprintf(fmtstr, args...)
|
||||
sb.WriteString(msg)
|
||||
if (l.use_color) {
|
||||
var code string
|
||||
code = l.log_level_to_ansi_code(level)
|
||||
sb.WriteString(code)
|
||||
sb.WriteString(msg)
|
||||
if code != "" { sb.WriteString("\x1B[0m") }
|
||||
} else {
|
||||
sb.WriteString(msg)
|
||||
}
|
||||
if msg[len(msg) - 1] != '\n' { sb.WriteRune('\n') }
|
||||
|
||||
// use queue to avoid blocking operation as much as possible
|
||||
@ -212,3 +249,24 @@ func (l *AppLogger) rotate() {
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
312
cmd/main.go
312
cmd/main.go
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import "context"
|
||||
import "crypto/tls"
|
||||
import _ "embed"
|
||||
import "flag"
|
||||
import "fmt"
|
||||
@ -10,10 +9,11 @@ import "io"
|
||||
import "net"
|
||||
import "os"
|
||||
import "os/signal"
|
||||
import "path/filepath"
|
||||
import "regexp"
|
||||
import "strings"
|
||||
import "sync"
|
||||
import "syscall"
|
||||
import "time"
|
||||
|
||||
// Don't change these items to 'const' as they can be overridden externally with a linker option
|
||||
var HODU_NAME string = "hodu"
|
||||
@ -23,6 +23,8 @@ var HODU_VERSION string = "0.0.0"
|
||||
var hodu_tls_cert_text []byte
|
||||
//go:embed tls.key
|
||||
var hodu_tls_key_text []byte
|
||||
//go:embed rsa.key
|
||||
var hodu_rsa_key_text []byte
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
type signal_handler struct {
|
||||
@ -64,7 +66,7 @@ chan_loop:
|
||||
}
|
||||
|
||||
func (sh *signal_handler) StartService(data interface{}) {
|
||||
// this isn't actually used standalone..
|
||||
// this isn't actually used standalone..
|
||||
// if we are to implement it, it must use the wait group for signal handler itself
|
||||
// however, this service is run through another service.
|
||||
// sh.wg.Add(1)
|
||||
@ -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 ctltlscfg *tls.Config
|
||||
var rpctlscfg *tls.Config
|
||||
var pxytlscfg *tls.Config
|
||||
var wpxtlscfg *tls.Config
|
||||
var ctl_prefix string
|
||||
var config *hodu.ServerConfig
|
||||
var logger *AppLogger
|
||||
var log_mask hodu.LogMask
|
||||
var logfile string
|
||||
var logmask hodu.LogMask
|
||||
var logfile_maxsize int64
|
||||
var logfile_rotate int
|
||||
var max_rpc_conns int
|
||||
var max_peers int
|
||||
var pty_user string
|
||||
var pty_shell string
|
||||
var xterm_html_file string
|
||||
var xterm_html string
|
||||
var err error
|
||||
|
||||
log_mask = hodu.LOG_ALL
|
||||
logmask = hodu.LOG_ALL
|
||||
|
||||
if cfg != nil {
|
||||
ctltlscfg, err = make_tls_server_config(&cfg.CTL.TLS)
|
||||
if err != nil { return err }
|
||||
rpctlscfg, err = make_tls_server_config(&cfg.RPC.TLS)
|
||||
if err != nil { return err }
|
||||
pxytlscfg, err = make_tls_server_config(&cfg.PXY.TLS)
|
||||
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
|
||||
config = &hodu.ServerConfig{
|
||||
CtlAddrs: ctl_addrs,
|
||||
RpcAddrs: rpc_addrs,
|
||||
RpxAddrs: rpx_addrs,
|
||||
PxyAddrs: pxy_addrs,
|
||||
WpxAddrs: wpx_addrs,
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if logfile == "" {
|
||||
logger = NewAppLogger("server", os.Stderr, log_mask)
|
||||
logger = NewAppLogger("server", os.Stderr, logmask)
|
||||
} 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
s, err = hodu.NewServer(
|
||||
context.Background(),
|
||||
logger,
|
||||
ctl_addrs,
|
||||
rpc_addrs,
|
||||
pxy_addrs,
|
||||
wpx_addrs,
|
||||
ctl_prefix,
|
||||
ctltlscfg,
|
||||
rpctlscfg,
|
||||
pxytlscfg,
|
||||
wpxtlscfg,
|
||||
max_rpc_conns,
|
||||
max_peers)
|
||||
s, err = hodu.NewServer(context.Background(), HODU_NAME, logger, config)
|
||||
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) }
|
||||
|
||||
s.StartService(nil)
|
||||
s.StartCtlService()
|
||||
s.StartRpxService()
|
||||
s.StartPxyService()
|
||||
s.StartWpxService()
|
||||
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])
|
||||
}
|
||||
|
||||
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 ctltlscfg *tls.Config
|
||||
var rpctlscfg *tls.Config
|
||||
var ctl_prefix string
|
||||
var cc hodu.ClientConfig
|
||||
var config *hodu.ClientConfig
|
||||
var cc hodu.ClientConnConfig
|
||||
var logger *AppLogger
|
||||
var log_mask hodu.LogMask
|
||||
var logfile string
|
||||
var logmask hodu.LogMask
|
||||
var logfile_maxsize int64
|
||||
var logfile_rotate int
|
||||
var max_rpc_conns int
|
||||
var max_peers int
|
||||
var peer_conn_tmout time.Duration
|
||||
var pty_user string
|
||||
var pty_shell string
|
||||
var xterm_html_file string
|
||||
var xterm_html string
|
||||
var i int
|
||||
var err error
|
||||
|
||||
log_mask = hodu.LOG_ALL
|
||||
if cfg != nil {
|
||||
ctltlscfg, err = make_tls_server_config(&cfg.CTL.TLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpctlscfg, err = make_tls_client_config(&cfg.RPC.TLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logmask = hodu.LOG_ALL
|
||||
|
||||
config = &hodu.ClientConfig{
|
||||
CtlAddrs: ctl_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(ctl_addrs) <= 0 { ctl_addrs = cfg.CTL.Service.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.ServerAuthority = cfg.RPC.Endpoint.Authority
|
||||
log_mask = log_strings_to_mask(cfg.APP.LogMask)
|
||||
logfile = cfg.APP.LogFile
|
||||
logmask = log_strings_to_mask(cfg.APP.LogMask)
|
||||
if logfile == "" { 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
|
||||
peer_conn_tmout = cfg.APP.PeerConnTmout
|
||||
pty_user = cfg.APP.PtyUser
|
||||
pty_shell = cfg.APP.PtyShell
|
||||
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.
|
||||
@ -306,23 +341,28 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
|
||||
}
|
||||
|
||||
if logfile == "" {
|
||||
logger = NewAppLogger("server", os.Stderr, log_mask)
|
||||
logger = NewAppLogger("client", os.Stderr, logmask)
|
||||
} 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 {
|
||||
return fmt.Errorf("failed to initialize logger - %s", err.Error())
|
||||
}
|
||||
}
|
||||
c = hodu.NewClient(
|
||||
context.Background(),
|
||||
logger,
|
||||
ctl_addrs,
|
||||
ctl_prefix,
|
||||
ctltlscfg,
|
||||
rpctlscfg,
|
||||
max_rpc_conns,
|
||||
max_peers,
|
||||
peer_conn_tmout)
|
||||
|
||||
if xterm_html_file != "" {
|
||||
var tmp []byte
|
||||
tmp, err = os.ReadFile(xterm_html_file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s - %s", xterm_html_file, err.Error())
|
||||
}
|
||||
xterm_html = string(tmp)
|
||||
}
|
||||
|
||||
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.StartCtlService() // control channel
|
||||
@ -342,14 +382,17 @@ func main() {
|
||||
if strings.EqualFold(os.Args[1], "server") {
|
||||
var rpc_addrs []string
|
||||
var ctl_addrs []string
|
||||
var rpx_addrs []string
|
||||
var pxy_addrs []string
|
||||
var wpx_addrs []string
|
||||
var cfgfile string
|
||||
var cfgpat string
|
||||
var logfile string
|
||||
var cfg *ServerConfig
|
||||
var cfg ServerConfig
|
||||
|
||||
ctl_addrs = make([]string, 0)
|
||||
rpc_addrs = make([]string, 0)
|
||||
rpx_addrs = make([]string, 0)
|
||||
pxy_addrs = make([]string, 0)
|
||||
wpx_addrs = make([]string, 0)
|
||||
|
||||
@ -362,6 +405,10 @@ func main() {
|
||||
rpc_addrs = append(rpc_addrs, v)
|
||||
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 {
|
||||
pxy_addrs = append(pxy_addrs, v)
|
||||
return nil
|
||||
@ -374,30 +421,50 @@ func main() {
|
||||
logfile = v
|
||||
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
|
||||
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
|
||||
err = flgs.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Printf ("ERROR: %s\n", err.Error())
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
|
||||
goto wrong_usage
|
||||
}
|
||||
|
||||
if flgs.NArg() > 0 { goto wrong_usage }
|
||||
|
||||
if cfgfile != "" {
|
||||
cfg, err = load_server_config(cfgfile)
|
||||
err = load_server_config_to(cfgfile, &cfg)
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: server error - %s\n", err.Error())
|
||||
goto oops
|
||||
@ -406,8 +473,9 @@ func main() {
|
||||
var rpc_addrs []string
|
||||
var ctl_addrs []string
|
||||
var cfgfile string
|
||||
var cfgpat string
|
||||
var logfile string
|
||||
var cfg *ClientConfig
|
||||
var cfg ClientConfig
|
||||
|
||||
ctl_addrs = make([]string, 0)
|
||||
rpc_addrs = make([]string, 0)
|
||||
@ -429,24 +497,44 @@ func main() {
|
||||
cfgfile = v
|
||||
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)
|
||||
err = flgs.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
fmt.Printf ("ERROR: %s\n", err.Error())
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err.Error())
|
||||
goto wrong_usage
|
||||
}
|
||||
|
||||
if cfgfile != "" {
|
||||
cfg, err = load_client_config(cfgfile)
|
||||
err = load_client_config_to(cfgfile, &cfg)
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: client error - %s\n", err.Error())
|
||||
goto oops
|
||||
@ -461,8 +549,8 @@ func main() {
|
||||
os.Exit(0)
|
||||
|
||||
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, " %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, "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] [--config-file-pattern=pattern] [peer-addr:peer-port ...]\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s version\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
|
||||
|
27
cmd/rsa.key
Normal file
27
cmd/rsa.key
Normal 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
15
go.mod
@ -3,13 +3,24 @@ module hodu
|
||||
go 1.22.0
|
||||
|
||||
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/net v0.28.0
|
||||
golang.org/x/sys v0.24.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
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
26
go.sum
@ -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/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/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
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/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
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
362
hodu.go
@ -1,15 +1,20 @@
|
||||
package hodu
|
||||
|
||||
import "bytes"
|
||||
import "crypto/rsa"
|
||||
import _ "embed"
|
||||
import "encoding/base64"
|
||||
import "fmt"
|
||||
import "net"
|
||||
import "net/http"
|
||||
import "net/netip"
|
||||
import "os"
|
||||
import "regexp"
|
||||
import "runtime"
|
||||
import "strings"
|
||||
import "sync"
|
||||
import "time"
|
||||
|
||||
|
||||
const HODU_RPC_VERSION uint32 = 0x010000
|
||||
|
||||
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 IPV6_PREFIX_ZERO = netip.MustParsePrefix("::/0")
|
||||
|
||||
type Named struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Write(id string, level LogLevel, 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{})
|
||||
}
|
||||
|
||||
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 {
|
||||
// the string is supposed to be addr:port
|
||||
|
||||
@ -53,7 +156,7 @@ func TcpAddrStrClass(addr string) string {
|
||||
ap, err = netip.ParseAddrPort(addr)
|
||||
if err == nil {
|
||||
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 {
|
||||
if addr.AddrPort().Addr().Is4() {
|
||||
var netip_addr netip.Addr
|
||||
netip_addr = addr.AddrPort().Addr()
|
||||
if netip_addr.Is4() || netip_addr.Is4In6() {
|
||||
return "tcp4"
|
||||
} else {
|
||||
return "tcp6"
|
||||
@ -76,8 +181,6 @@ func word_to_route_option(word string) RouteOption {
|
||||
return RouteOption(ROUTE_OPTION_TCP6)
|
||||
case "tcp":
|
||||
return RouteOption(ROUTE_OPTION_TCP)
|
||||
case "tty":
|
||||
return RouteOption(ROUTE_OPTION_TTY)
|
||||
case "http":
|
||||
return RouteOption(ROUTE_OPTION_HTTP)
|
||||
case "https":
|
||||
@ -89,7 +192,7 @@ func word_to_route_option(word string) RouteOption {
|
||||
return RouteOption(ROUTE_OPTION_UNSPEC)
|
||||
}
|
||||
|
||||
func string_to_route_option(desc string) RouteOption {
|
||||
func StringToRouteOption(desc string) RouteOption {
|
||||
var fld string
|
||||
var option RouteOption
|
||||
var p RouteOption
|
||||
@ -103,13 +206,12 @@ func string_to_route_option(desc string) RouteOption {
|
||||
return option
|
||||
}
|
||||
|
||||
func (option RouteOption) string() string {
|
||||
func (option RouteOption) String() string {
|
||||
var str string
|
||||
str = ""
|
||||
if option & RouteOption(ROUTE_OPTION_TCP6) != 0 { str += " tcp6" }
|
||||
if option & RouteOption(ROUTE_OPTION_TCP4) != 0 { str += " tcp4" }
|
||||
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_HTTPS) != 0 { str += " https" }
|
||||
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{}) {
|
||||
var buf []byte
|
||||
buf = make([]byte, 65536); buf = buf[:min(65536, runtime.Stack(buf, false))]
|
||||
log.Write("", LOG_ERROR, "[%s] %s %s - %v\n%s", req.RemoteAddr, req.Method, req.URL.String(), err, string(buf))
|
||||
buf = make([]byte, 65536)
|
||||
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()
|
||||
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]
|
||||
}
|
||||
|
||||
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
|
||||
// the decimal point. For example, 0.05 to mean 500ms.
|
||||
// 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)
|
||||
}
|
||||
|
||||
func DurationToSecString(d time.Duration) string {
|
||||
return fmt.Sprintf("%.09f", d.Seconds())
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
func WriteJsonRespHeader(w http.ResponseWriter, status_code int) int {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(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.WriteHeader(status_code)
|
||||
return status_code
|
||||
@ -195,6 +304,8 @@ func WriteEmptyRespHeader(w http.ResponseWriter, status_code int) int {
|
||||
return status_code
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
func server_route_to_proxy_info(r *ServerRoute) *ServerRouteProxyInfo {
|
||||
return &ServerRouteProxyInfo{
|
||||
SvcOption: r.SvcOption,
|
||||
@ -214,3 +325,230 @@ func proxy_info_to_server_route(pi *ServerRouteProxyInfo) *ServerRoute {
|
||||
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]
|
||||
}
|
725
hodu.pb.go
725
hodu.pb.go
@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc-gen-go v1.36.7
|
||||
// protoc v3.19.6
|
||||
// source: hodu.proto
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,10 +28,9 @@ const (
|
||||
ROUTE_OPTION_TCP ROUTE_OPTION = 1
|
||||
ROUTE_OPTION_TCP4 ROUTE_OPTION = 2
|
||||
ROUTE_OPTION_TCP6 ROUTE_OPTION = 4
|
||||
ROUTE_OPTION_TTY ROUTE_OPTION = 8
|
||||
ROUTE_OPTION_HTTP ROUTE_OPTION = 16
|
||||
ROUTE_OPTION_HTTPS ROUTE_OPTION = 32
|
||||
ROUTE_OPTION_SSH ROUTE_OPTION = 64
|
||||
ROUTE_OPTION_HTTP ROUTE_OPTION = 8
|
||||
ROUTE_OPTION_HTTPS ROUTE_OPTION = 16
|
||||
ROUTE_OPTION_SSH ROUTE_OPTION = 32
|
||||
)
|
||||
|
||||
// Enum value maps for ROUTE_OPTION.
|
||||
@ -40,20 +40,18 @@ var (
|
||||
1: "TCP",
|
||||
2: "TCP4",
|
||||
4: "TCP6",
|
||||
8: "TTY",
|
||||
16: "HTTP",
|
||||
32: "HTTPS",
|
||||
64: "SSH",
|
||||
8: "HTTP",
|
||||
16: "HTTPS",
|
||||
32: "SSH",
|
||||
}
|
||||
ROUTE_OPTION_value = map[string]int32{
|
||||
"UNSPEC": 0,
|
||||
"TCP": 1,
|
||||
"TCP4": 2,
|
||||
"TCP6": 4,
|
||||
"TTY": 8,
|
||||
"HTTP": 16,
|
||||
"HTTPS": 32,
|
||||
"SSH": 64,
|
||||
"HTTP": 8,
|
||||
"HTTPS": 16,
|
||||
"SSH": 32,
|
||||
}
|
||||
)
|
||||
|
||||
@ -97,21 +95,43 @@ const (
|
||||
PACKET_KIND_PEER_ABORTED PACKET_KIND = 7
|
||||
PACKET_KIND_PEER_EOF PACKET_KIND = 8
|
||||
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.
|
||||
var (
|
||||
PACKET_KIND_name = map[int32]string{
|
||||
0: "RESERVED",
|
||||
1: "ROUTE_START",
|
||||
2: "ROUTE_STOP",
|
||||
3: "ROUTE_STARTED",
|
||||
4: "ROUTE_STOPPED",
|
||||
5: "PEER_STARTED",
|
||||
6: "PEER_STOPPED",
|
||||
7: "PEER_ABORTED",
|
||||
8: "PEER_EOF",
|
||||
9: "PEER_DATA",
|
||||
0: "RESERVED",
|
||||
1: "ROUTE_START",
|
||||
2: "ROUTE_STOP",
|
||||
3: "ROUTE_STARTED",
|
||||
4: "ROUTE_STOPPED",
|
||||
5: "PEER_STARTED",
|
||||
6: "PEER_STOPPED",
|
||||
7: "PEER_ABORTED",
|
||||
8: "PEER_EOF",
|
||||
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{
|
||||
"RESERVED": 0,
|
||||
@ -124,6 +144,17 @@ var (
|
||||
"PEER_ABORTED": 7,
|
||||
"PEER_EOF": 8,
|
||||
"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 {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Version uint32 `protobuf:"varint,1,opt,name=Version,proto3" json:"Version,omitempty"`
|
||||
Flags uint64 `protobuf:"varint,2,opt,name=Flags,proto3" json:"Flags,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Version uint32 `protobuf:"varint,1,opt,name=Version,proto3" json:"Version,omitempty"`
|
||||
Flags uint64 `protobuf:"varint,2,opt,name=Flags,proto3" json:"Flags,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Seed) Reset() {
|
||||
@ -208,30 +238,29 @@ func (x *Seed) GetFlags() uint64 {
|
||||
}
|
||||
|
||||
type RouteDesc struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"`
|
||||
// C->S(ROUTE_START): client-side peer address
|
||||
// S->C(ROUTE_STARTED): server-side listening address
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,omitempty"`
|
||||
// C->S(ROUTE_START/STOP): client-side peer address
|
||||
// S->C(ROUTE_STARTED/STOPPED): server-side listening address
|
||||
TargetAddrStr string `protobuf:"bytes,2,opt,name=TargetAddrStr,proto3" json:"TargetAddrStr,omitempty"`
|
||||
// C->S(ROUTE_START): human-readable name of client-side peer
|
||||
// S->C(ROUTE_STARTED): clone as sent by C
|
||||
// C->S(ROUTE_START/STOPPED): human-readable name of client-side peer
|
||||
// S->C(ROUTE_STARTED/STOPPED): clone as sent by C
|
||||
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 client-side peer(e.g. tty, http, https)
|
||||
//
|
||||
// S->C(ROUTE_STARTED): cloned as sent by C.
|
||||
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
|
||||
ServiceAddrStr string `protobuf:"bytes,5,opt,name=ServiceAddrStr,proto3" json:"ServiceAddrStr,omitempty"`
|
||||
// C->S(ROUTE_START): permitted network of server-side peers.
|
||||
// S->C(ROUTE_STARTED): cloned as sent by C.
|
||||
// C->S(ROUTE_START): requested permitted network of server-side peers.
|
||||
// S->C(ROUTE_STARTED): actual permitted network of server-side peers
|
||||
ServiceNetStr string `protobuf:"bytes,6,opt,name=ServiceNetStr,proto3" json:"ServiceNetStr,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RouteDesc) Reset() {
|
||||
@ -307,14 +336,13 @@ func (x *RouteDesc) GetServiceNetStr() string {
|
||||
}
|
||||
|
||||
type PeerDesc struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,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"`
|
||||
LocalAddrStr string `protobuf:"bytes,4,opt,name=LocalAddrStr,proto3" json:"LocalAddrStr,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,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"`
|
||||
LocalAddrStr string `protobuf:"bytes,4,opt,name=LocalAddrStr,proto3" json:"LocalAddrStr,omitempty"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PeerDesc) Reset() {
|
||||
@ -376,13 +404,12 @@ func (x *PeerDesc) GetLocalAddrStr() string {
|
||||
}
|
||||
|
||||
type PeerData struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,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"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
RouteId uint32 `protobuf:"varint,1,opt,name=RouteId,proto3" json:"RouteId,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"`
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PeerData) Reset() {
|
||||
@ -436,23 +463,271 @@ func (x *PeerData) GetData() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Packet struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
type ConnDesc struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Token string `protobuf:"bytes,1,opt,name=Token,proto3" json:"Token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
Kind PACKET_KIND `protobuf:"varint,1,opt,name=Kind,proto3,enum=PACKET_KIND" json:"Kind,omitempty"`
|
||||
// Types that are assignable to U:
|
||||
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"`
|
||||
// Types that are valid to be assigned to U:
|
||||
//
|
||||
// *Packet_Route
|
||||
// *Packet_Peer
|
||||
// *Packet_Data
|
||||
U isPacket_U `protobuf_oneof:"U"`
|
||||
// *Packet_Conn
|
||||
// *Packet_ConnErr
|
||||
// *Packet_ConnNoti
|
||||
// *Packet_RptyEvt
|
||||
// *Packet_RpxEvt
|
||||
U isPacket_U `protobuf_oneof:"U"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Packet) Reset() {
|
||||
*x = Packet{}
|
||||
mi := &file_hodu_proto_msgTypes[4]
|
||||
mi := &file_hodu_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -464,7 +739,7 @@ func (x *Packet) String() string {
|
||||
func (*Packet) ProtoMessage() {}
|
||||
|
||||
func (x *Packet) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hodu_proto_msgTypes[4]
|
||||
mi := &file_hodu_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -477,7 +752,7 @@ func (x *Packet) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Packet.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -487,30 +762,81 @@ func (x *Packet) GetKind() PACKET_KIND {
|
||||
return PACKET_KIND_RESERVED
|
||||
}
|
||||
|
||||
func (m *Packet) GetU() isPacket_U {
|
||||
if m != nil {
|
||||
return m.U
|
||||
func (x *Packet) GetU() isPacket_U {
|
||||
if x != nil {
|
||||
return x.U
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Packet) GetRoute() *RouteDesc {
|
||||
if x, ok := x.GetU().(*Packet_Route); ok {
|
||||
return x.Route
|
||||
if x != nil {
|
||||
if x, ok := x.U.(*Packet_Route); ok {
|
||||
return x.Route
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Packet) GetPeer() *PeerDesc {
|
||||
if x, ok := x.GetU().(*Packet_Peer); ok {
|
||||
return x.Peer
|
||||
if x != nil {
|
||||
if x, ok := x.U.(*Packet_Peer); ok {
|
||||
return x.Peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Packet) GetData() *PeerData {
|
||||
if x, ok := x.GetU().(*Packet_Data); ok {
|
||||
return x.Data
|
||||
if x != nil {
|
||||
if x, ok := x.U.(*Packet_Data); ok {
|
||||
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
|
||||
}
|
||||
@ -531,119 +857,182 @@ type Packet_Data struct {
|
||||
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_Peer) 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_rawDesc = []byte{
|
||||
0x0a, 0x0a, 0x68, 0x6f, 0x64, 0x75, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x36, 0x0a, 0x04,
|
||||
0x53, 0x65, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14,
|
||||
0x0a, 0x05, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x46,
|
||||
0x6c, 0x61, 0x67, 0x73, 0x22, 0xdf, 0x01, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x44, 0x65,
|
||||
0x73, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0d,
|
||||
0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x53,
|
||||
0x74, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x70, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||
0x63, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72,
|
||||
0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x65, 0x74, 0x53, 0x74,
|
||||
0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||
0x4e, 0x65, 0x74, 0x53, 0x74, 0x72, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x44,
|
||||
0x65, 0x73, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a,
|
||||
0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x50,
|
||||
0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41,
|
||||
0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65,
|
||||
0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x4c,
|
||||
0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0c, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x53, 0x74, 0x72, 0x22,
|
||||
0x50, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x52,
|
||||
0x6f, 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x52, 0x6f,
|
||||
0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a,
|
||||
0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x44, 0x61, 0x74,
|
||||
0x61, 0x22, 0x95, 0x01, 0x0a, 0x06, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x20, 0x0a, 0x04,
|
||||
0x4b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x50, 0x41, 0x43,
|
||||
0x4b, 0x45, 0x54, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x22,
|
||||
0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e,
|
||||
0x52, 0x6f, 0x75, 0x74, 0x65, 0x44, 0x65, 0x73, 0x63, 0x48, 0x00, 0x52, 0x05, 0x52, 0x6f, 0x75,
|
||||
0x74, 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x09, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x65, 0x73, 0x63, 0x48, 0x00, 0x52, 0x04, 0x50,
|
||||
0x65, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x09, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x04,
|
||||
0x44, 0x61, 0x74, 0x61, 0x42, 0x03, 0x0a, 0x01, 0x55, 0x2a, 0x5e, 0x0a, 0x0c, 0x52, 0x4f, 0x55,
|
||||
0x54, 0x45, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x4e, 0x53,
|
||||
0x50, 0x45, 0x43, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08,
|
||||
0x0a, 0x04, 0x54, 0x43, 0x50, 0x34, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x43, 0x50, 0x36,
|
||||
0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x10, 0x08, 0x12, 0x08, 0x0a, 0x04, 0x48,
|
||||
0x54, 0x54, 0x50, 0x10, 0x10, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x20,
|
||||
0x12, 0x07, 0x0a, 0x03, 0x53, 0x53, 0x48, 0x10, 0x40, 0x2a, 0xb5, 0x01, 0x0a, 0x0b, 0x50, 0x41,
|
||||
0x43, 0x4b, 0x45, 0x54, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53,
|
||||
0x45, 0x52, 0x56, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x4f, 0x55, 0x54, 0x45,
|
||||
0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x4f, 0x55, 0x54,
|
||||
0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x55, 0x54,
|
||||
0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x52,
|
||||
0x4f, 0x55, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x04, 0x12, 0x10,
|
||||
0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x05,
|
||||
0x12, 0x10, 0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44,
|
||||
0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x41, 0x42, 0x4f, 0x52, 0x54,
|
||||
0x45, 0x44, 0x10, 0x07, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x45, 0x4f, 0x46,
|
||||
0x10, 0x08, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x10,
|
||||
0x09, 0x32, 0x49, 0x0a, 0x04, 0x48, 0x6f, 0x64, 0x75, 0x12, 0x19, 0x0a, 0x07, 0x47, 0x65, 0x74,
|
||||
0x53, 0x65, 0x65, 0x64, 0x12, 0x05, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x1a, 0x05, 0x2e, 0x53, 0x65,
|
||||
0x65, 0x64, 0x22, 0x00, 0x12, 0x26, 0x0a, 0x0c, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x74,
|
||||
0x72, 0x65, 0x61, 0x6d, 0x12, 0x07, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x1a, 0x07, 0x2e,
|
||||
0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06,
|
||||
0x2e, 0x2f, 0x68, 0x6f, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
const file_hodu_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"hodu.proto\"6\n" +
|
||||
"\x04Seed\x12\x18\n" +
|
||||
"\aVersion\x18\x01 \x01(\rR\aVersion\x12\x14\n" +
|
||||
"\x05Flags\x18\x02 \x01(\x04R\x05Flags\"\xdf\x01\n" +
|
||||
"\tRouteDesc\x12\x18\n" +
|
||||
"\aRouteId\x18\x01 \x01(\rR\aRouteId\x12$\n" +
|
||||
"\rTargetAddrStr\x18\x02 \x01(\tR\rTargetAddrStr\x12\x1e\n" +
|
||||
"\n" +
|
||||
"TargetName\x18\x03 \x01(\tR\n" +
|
||||
"TargetName\x12$\n" +
|
||||
"\rServiceOption\x18\x04 \x01(\rR\rServiceOption\x12&\n" +
|
||||
"\x0eServiceAddrStr\x18\x05 \x01(\tR\x0eServiceAddrStr\x12$\n" +
|
||||
"\rServiceNetStr\x18\x06 \x01(\tR\rServiceNetStr\"\x86\x01\n" +
|
||||
"\bPeerDesc\x12\x18\n" +
|
||||
"\aRouteId\x18\x01 \x01(\rR\aRouteId\x12\x16\n" +
|
||||
"\x06PeerId\x18\x02 \x01(\rR\x06PeerId\x12$\n" +
|
||||
"\rRemoteAddrStr\x18\x03 \x01(\tR\rRemoteAddrStr\x12\"\n" +
|
||||
"\fLocalAddrStr\x18\x04 \x01(\tR\fLocalAddrStr\"P\n" +
|
||||
"\bPeerData\x12\x18\n" +
|
||||
"\aRouteId\x18\x01 \x01(\rR\aRouteId\x12\x16\n" +
|
||||
"\x06PeerId\x18\x02 \x01(\rR\x06PeerId\x12\x12\n" +
|
||||
"\x04Data\x18\x03 \x01(\fR\x04Data\" \n" +
|
||||
"\bConnDesc\x12\x14\n" +
|
||||
"\x05Token\x18\x01 \x01(\tR\x05Token\"9\n" +
|
||||
"\tConnError\x12\x18\n" +
|
||||
"\aErrorId\x18\x01 \x01(\rR\aErrorId\x12\x12\n" +
|
||||
"\x04Text\x18\x02 \x01(\tR\x04Text\" \n" +
|
||||
"\n" +
|
||||
"ConnNotice\x12\x12\n" +
|
||||
"\x04Text\x18\x01 \x01(\tR\x04Text\"/\n" +
|
||||
"\tRptyEvent\x12\x0e\n" +
|
||||
"\x02Id\x18\x01 \x01(\x04R\x02Id\x12\x12\n" +
|
||||
"\x04Data\x18\x02 \x01(\fR\x04Data\".\n" +
|
||||
"\bRpxEvent\x12\x0e\n" +
|
||||
"\x02Id\x18\x01 \x01(\x04R\x02Id\x12\x12\n" +
|
||||
"\x04Data\x18\x02 \x01(\fR\x04Data\"\xd6\x02\n" +
|
||||
"\x06Packet\x12 \n" +
|
||||
"\x04Kind\x18\x01 \x01(\x0e2\f.PACKET_KINDR\x04Kind\x12\"\n" +
|
||||
"\x05Route\x18\x02 \x01(\v2\n" +
|
||||
".RouteDescH\x00R\x05Route\x12\x1f\n" +
|
||||
"\x04Peer\x18\x03 \x01(\v2\t.PeerDescH\x00R\x04Peer\x12\x1f\n" +
|
||||
"\x04Data\x18\x04 \x01(\v2\t.PeerDataH\x00R\x04Data\x12\x1f\n" +
|
||||
"\x04Conn\x18\x05 \x01(\v2\t.ConnDescH\x00R\x04Conn\x12&\n" +
|
||||
"\aConnErr\x18\x06 \x01(\v2\n" +
|
||||
".ConnErrorH\x00R\aConnErr\x12)\n" +
|
||||
"\bConnNoti\x18\a \x01(\v2\v.ConnNoticeH\x00R\bConnNoti\x12&\n" +
|
||||
"\aRptyEvt\x18\b \x01(\v2\n" +
|
||||
".RptyEventH\x00R\aRptyEvt\x12#\n" +
|
||||
"\x06RpxEvt\x18\t \x01(\v2\t.RpxEventH\x00R\x06RpxEvtB\x03\n" +
|
||||
"\x01U*U\n" +
|
||||
"\fROUTE_OPTION\x12\n" +
|
||||
"\n" +
|
||||
"\x06UNSPEC\x10\x00\x12\a\n" +
|
||||
"\x03TCP\x10\x01\x12\b\n" +
|
||||
"\x04TCP4\x10\x02\x12\b\n" +
|
||||
"\x04TCP6\x10\x04\x12\b\n" +
|
||||
"\x04HTTP\x10\b\x12\t\n" +
|
||||
"\x05HTTPS\x10\x10\x12\a\n" +
|
||||
"\x03SSH\x10 *\xda\x02\n" +
|
||||
"\vPACKET_KIND\x12\f\n" +
|
||||
"\bRESERVED\x10\x00\x12\x0f\n" +
|
||||
"\vROUTE_START\x10\x01\x12\x0e\n" +
|
||||
"\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 (
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
(ROUTE_OPTION)(0), // 0: ROUTE_OPTION
|
||||
(PACKET_KIND)(0), // 1: PACKET_KIND
|
||||
(*Seed)(nil), // 2: Seed
|
||||
(*RouteDesc)(nil), // 3: RouteDesc
|
||||
(*PeerDesc)(nil), // 4: PeerDesc
|
||||
(*PeerData)(nil), // 5: PeerData
|
||||
(*Packet)(nil), // 6: Packet
|
||||
(ROUTE_OPTION)(0), // 0: ROUTE_OPTION
|
||||
(PACKET_KIND)(0), // 1: PACKET_KIND
|
||||
(*Seed)(nil), // 2: Seed
|
||||
(*RouteDesc)(nil), // 3: RouteDesc
|
||||
(*PeerDesc)(nil), // 4: PeerDesc
|
||||
(*PeerData)(nil), // 5: PeerData
|
||||
(*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{
|
||||
1, // 0: Packet.Kind:type_name -> PACKET_KIND
|
||||
3, // 1: Packet.Route:type_name -> RouteDesc
|
||||
4, // 2: Packet.Peer:type_name -> PeerDesc
|
||||
5, // 3: Packet.Data:type_name -> PeerData
|
||||
2, // 4: Hodu.GetSeed:input_type -> Seed
|
||||
6, // 5: Hodu.PacketStream:input_type -> Packet
|
||||
2, // 6: Hodu.GetSeed:output_type -> Seed
|
||||
6, // 7: Hodu.PacketStream:output_type -> Packet
|
||||
6, // [6:8] is the sub-list for method output_type
|
||||
4, // [4:6] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
1, // 0: Packet.Kind:type_name -> PACKET_KIND
|
||||
3, // 1: Packet.Route:type_name -> RouteDesc
|
||||
4, // 2: Packet.Peer:type_name -> PeerDesc
|
||||
5, // 3: Packet.Data:type_name -> PeerData
|
||||
6, // 4: Packet.Conn:type_name -> ConnDesc
|
||||
7, // 5: Packet.ConnErr:type_name -> ConnError
|
||||
8, // 6: Packet.ConnNoti:type_name -> ConnNotice
|
||||
9, // 7: Packet.RptyEvt:type_name -> RptyEvent
|
||||
10, // 8: Packet.RpxEvt:type_name -> RpxEvent
|
||||
2, // 9: Hodu.GetSeed:input_type -> Seed
|
||||
11, // 10: Hodu.PacketStream:input_type -> Packet
|
||||
2, // 11: Hodu.GetSeed:output_type -> Seed
|
||||
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() }
|
||||
@ -651,18 +1040,23 @@ func file_hodu_proto_init() {
|
||||
if File_hodu_proto != nil {
|
||||
return
|
||||
}
|
||||
file_hodu_proto_msgTypes[4].OneofWrappers = []any{
|
||||
file_hodu_proto_msgTypes[9].OneofWrappers = []any{
|
||||
(*Packet_Route)(nil),
|
||||
(*Packet_Peer)(nil),
|
||||
(*Packet_Data)(nil),
|
||||
(*Packet_Conn)(nil),
|
||||
(*Packet_ConnErr)(nil),
|
||||
(*Packet_ConnNoti)(nil),
|
||||
(*Packet_RptyEvt)(nil),
|
||||
(*Packet_RpxEvt)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
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,
|
||||
NumMessages: 5,
|
||||
NumMessages: 10,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
@ -672,7 +1066,6 @@ func file_hodu_proto_init() {
|
||||
MessageInfos: file_hodu_proto_msgTypes,
|
||||
}.Build()
|
||||
File_hodu_proto = out.File
|
||||
file_hodu_proto_rawDesc = nil
|
||||
file_hodu_proto_goTypes = nil
|
||||
file_hodu_proto_depIdxs = nil
|
||||
}
|
||||
|
74
hodu.proto
74
hodu.proto
@ -23,35 +23,34 @@ enum ROUTE_OPTION {
|
||||
TCP = 1;
|
||||
TCP4 = 2;
|
||||
TCP6 = 4;
|
||||
TTY = 8;
|
||||
HTTP = 16;
|
||||
HTTPS = 32;
|
||||
SSH = 64;
|
||||
HTTP = 8;
|
||||
HTTPS = 16;
|
||||
SSH = 32;
|
||||
};
|
||||
|
||||
message RouteDesc {
|
||||
uint32 RouteId = 1;
|
||||
|
||||
// C->S(ROUTE_START): client-side peer address
|
||||
// S->C(ROUTE_STARTED): server-side listening address
|
||||
// C->S(ROUTE_START/STOP): client-side peer address
|
||||
// S->C(ROUTE_STARTED/STOPPED): server-side listening address
|
||||
string TargetAddrStr = 2;
|
||||
|
||||
// C->S(ROUTE_START): human-readable name of client-side peer
|
||||
// S->C(ROUTE_STARTED): clone as sent by C
|
||||
// C->S(ROUTE_START/STOPPED): human-readable name of client-side peer
|
||||
// S->C(ROUTE_STARTED/STOPPED): clone as sent by C
|
||||
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 client-side peer(e.g. tty, http, https)
|
||||
// S->C(ROUTE_STARTED): cloned as sent by C.
|
||||
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
|
||||
string ServiceAddrStr = 5;
|
||||
|
||||
// C->S(ROUTE_START): permitted network of server-side peers.
|
||||
// S->C(ROUTE_STARTED): cloned as sent by C.
|
||||
// C->S(ROUTE_START): requested permitted network of server-side peers.
|
||||
// S->C(ROUTE_STARTED): actual permitted network of server-side peers
|
||||
string ServiceNetStr = 6;
|
||||
};
|
||||
|
||||
@ -64,8 +63,31 @@ message PeerDesc {
|
||||
|
||||
message PeerData {
|
||||
uint32 RouteId = 1;
|
||||
uint32 PeerId = 2;
|
||||
bytes Data = 3;
|
||||
uint32 PeerId = 2;
|
||||
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 {
|
||||
@ -79,14 +101,32 @@ enum PACKET_KIND {
|
||||
PEER_ABORTED = 7;
|
||||
PEER_EOF = 8;
|
||||
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 {
|
||||
PACKET_KIND Kind = 1;
|
||||
|
||||
oneof U {
|
||||
RouteDesc Route = 2;
|
||||
PeerDesc Peer = 3;
|
||||
PeerData Data = 4;
|
||||
RouteDesc Route = 2;
|
||||
PeerDesc Peer = 3;
|
||||
PeerData Data = 4;
|
||||
ConnDesc Conn = 5;
|
||||
ConnError ConnErr = 6;
|
||||
ConnNotice ConnNoti = 7;
|
||||
RptyEvent RptyEvt = 8;
|
||||
RpxEvent RpxEvt = 9;
|
||||
};
|
||||
}
|
||||
|
134
jwt.go
Normal file
134
jwt.go
Normal 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
39
jwt_test.go
Normal 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") }
|
||||
}
|
47
packet.go
47
packet.go
@ -62,3 +62,50 @@ func MakePeerDataPacket(route_id RouteId, peer_id PeerId, data []byte) *Packet {
|
||||
return &Packet{Kind: PACKET_KIND_PEER_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
71
pty.go
Normal 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
|
||||
}
|
||||
|
870
server-ctl.go
870
server-ctl.go
File diff suppressed because it is too large
Load Diff
139
server-metrics.go
Normal file
139
server-metrics.go
Normal 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()),
|
||||
)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package hodu
|
||||
|
||||
import "container/list"
|
||||
import "context"
|
||||
import "errors"
|
||||
import "io"
|
||||
@ -10,10 +11,13 @@ import "sync/atomic"
|
||||
import "time"
|
||||
|
||||
type ServerPeerConn struct {
|
||||
route *ServerRoute
|
||||
conn_id PeerId
|
||||
cts *ClientConn
|
||||
conn *net.TCPConn
|
||||
route *ServerRoute
|
||||
conn_id PeerId
|
||||
conn *net.TCPConn
|
||||
Created time.Time
|
||||
|
||||
node_in_server *list.Element
|
||||
node_in_conn *list.Element
|
||||
|
||||
stop_chan chan bool
|
||||
stop_req atomic.Bool
|
||||
@ -22,6 +26,8 @@ type ServerPeerConn struct {
|
||||
client_peer_started atomic.Bool
|
||||
client_peer_stopped 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 {
|
||||
@ -30,6 +36,7 @@ func NewServerPeerConn(r *ServerRoute, c *net.TCPConn, id PeerId) *ServerPeerCon
|
||||
spc.route = r
|
||||
spc.conn = c
|
||||
spc.conn_id = id
|
||||
spc.Created = time.Now()
|
||||
|
||||
spc.stop_chan = make(chan bool, 8)
|
||||
spc.stop_req.Store(false)
|
||||
@ -54,21 +61,23 @@ func (spc *ServerPeerConn) RunTask(wg *sync.WaitGroup) {
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
spc.route.Cts.S.FirePeerEvent(SERVER_EVENT_PEER_STARTED, spc)
|
||||
|
||||
conn_raddr = spc.conn.RemoteAddr().String()
|
||||
conn_laddr = spc.conn.LocalAddr().String()
|
||||
|
||||
pss = spc.route.Cts.pss
|
||||
err = pss.Send(MakePeerStartedPacket(spc.route.Id, spc.conn_id, conn_raddr, conn_laddr))
|
||||
if err != nil {
|
||||
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR,
|
||||
"Failed to send peer_started event(%d,%d,%s,%s) to client - %s",
|
||||
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
|
||||
"Failed to send %s event(%d,%d,%s,%s) to client - %s",
|
||||
PACKET_KIND_PEER_STARTED.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
goto done_without_stop
|
||||
}
|
||||
|
||||
// set up a timer to set waiting duration until the connection is
|
||||
// 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:
|
||||
for {
|
||||
select {
|
||||
@ -93,31 +102,33 @@ wait_for_started:
|
||||
|
||||
for {
|
||||
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 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))
|
||||
if err != nil {
|
||||
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR,
|
||||
"Failed to send peer_eof event(%d,%d,%s,%s) to client - %s",
|
||||
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
|
||||
"Failed to send %s event(%d,%d,%s,%s) to client - %s",
|
||||
PACKET_KIND_PEER_EOF.String(), spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
goto done
|
||||
}
|
||||
goto wait_for_stopped
|
||||
} 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",
|
||||
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
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:
|
||||
@ -133,15 +144,27 @@ wait_for_stopped:
|
||||
done:
|
||||
err = pss.Send(MakePeerStoppedPacket(spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), spc.conn.LocalAddr().String()))
|
||||
if err != nil {
|
||||
spc.route.Cts.svr.log.Write(spc.route.Cts.sid, LOG_ERROR,
|
||||
"Failed to send peer_stopped(%d,%d,%s,%s) to client - %s",
|
||||
spc.route.Id, spc.conn_id, conn_raddr, conn_laddr, err.Error())
|
||||
spc.route.Cts.S.log.Write(spc.route.Cts.Sid, LOG_ERROR,
|
||||
"Failed to send %s(%d,%d,%s,%s) to client - %s",
|
||||
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
|
||||
}
|
||||
|
||||
done_without_stop:
|
||||
spc.ReqStop()
|
||||
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() {
|
||||
@ -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:
|
||||
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) {
|
||||
spc.client_peer_status_chan <- true
|
||||
}
|
||||
|
||||
spc.route.Cts.S.FirePeerEvent(SERVER_EVENT_PEER_UPDATED, spc)
|
||||
|
||||
case PACKET_KIND_PEER_ABORTED:
|
||||
spc.ReqStop()
|
||||
|
||||
@ -192,21 +230,21 @@ func (spc *ServerPeerConn) ReportEvent(event_type PACKET_KIND, event_data interf
|
||||
var err error
|
||||
_, err = spc.conn.Write(data)
|
||||
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",
|
||||
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String(), err.Error())
|
||||
spc.ReqStop()
|
||||
}
|
||||
} else {
|
||||
// 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)",
|
||||
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String())
|
||||
spc.ReqStop()
|
||||
}
|
||||
} else {
|
||||
// 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)",
|
||||
spc.route.Cts.RemoteAddr, spc.route.Id, spc.conn_id, spc.conn.RemoteAddr().String())
|
||||
spc.ReqStop()
|
||||
|
400
server-pty.go
Normal file
400
server-pty.go
Normal 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
|
||||
}
|
@ -3,8 +3,8 @@ package hodu
|
||||
import "bufio"
|
||||
import "context"
|
||||
import "crypto/tls"
|
||||
import _ "embed"
|
||||
import "encoding/json"
|
||||
import "errors"
|
||||
import "fmt"
|
||||
import "io"
|
||||
import "net"
|
||||
@ -21,32 +21,23 @@ import "golang.org/x/crypto/ssh"
|
||||
import "golang.org/x/net/http/httpguts"
|
||||
import "golang.org/x/net/websocket"
|
||||
|
||||
//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 server_proxy struct {
|
||||
s *Server
|
||||
id string
|
||||
type server_pxy struct {
|
||||
S *Server
|
||||
Id string
|
||||
}
|
||||
|
||||
type server_proxy_http_main struct {
|
||||
server_proxy
|
||||
type server_pxy_http_main struct {
|
||||
server_pxy
|
||||
prefix string
|
||||
}
|
||||
|
||||
type server_proxy_xterm_file struct {
|
||||
server_proxy
|
||||
type server_pxy_xterm_file struct {
|
||||
server_pxy
|
||||
file string
|
||||
}
|
||||
|
||||
type server_proxy_http_wpx struct {
|
||||
server_proxy
|
||||
type server_pxy_http_wpx struct {
|
||||
server_pxy
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 route_id string
|
||||
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 err error
|
||||
|
||||
s = pxy.S
|
||||
|
||||
if in_wpx_mode { // for wpx
|
||||
conn_id = req.PathValue("port_id")
|
||||
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)
|
||||
}
|
||||
|
||||
r, err = pxy.s.FindServerRouteByIdStr(conn_id, route_id)
|
||||
r, err = s.FindServerRouteByIdStr(conn_id, route_id)
|
||||
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
|
||||
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 }
|
||||
pi.IsForeign = true // just to ensure this
|
||||
} else {
|
||||
@ -234,7 +238,7 @@ func (pxy *server_proxy_http_main) get_route_proxy_info(req *http.Request, in_wp
|
||||
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 proxy_res_body io.ReadWriteCloser
|
||||
var rc *http.ResponseController
|
||||
@ -285,30 +289,36 @@ func (pxy *server_proxy_http_main) serve_upgraded(w http.ResponseWriter, req *ht
|
||||
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 waitctx context.Context
|
||||
var cancel_wait context.CancelFunc
|
||||
var conn net.Conn
|
||||
var tls_config *tls.Config
|
||||
var err error
|
||||
|
||||
// establish the connection.
|
||||
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())
|
||||
cancel_wait()
|
||||
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
|
||||
return &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // TODO: make this part configurable?
|
||||
TLSClientConfig: tls_config,
|
||||
}, 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_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 pi *ServerRouteProxyInfo
|
||||
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 err error
|
||||
|
||||
s = pxy.s
|
||||
s = pxy.S
|
||||
in_wpx_mode = (pxy.prefix == PORT_ID_MARKER)
|
||||
|
||||
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)
|
||||
transport, err = pxy.addr_to_transport(s.ctx, addr)
|
||||
transport, err = pxy.addr_to_transport(s.Ctx, addr)
|
||||
if err != nil {
|
||||
status_code = WriteEmptyRespHeader(w, http.StatusBadGateway)
|
||||
goto oops
|
||||
}
|
||||
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 {
|
||||
status_code = WriteEmptyRespHeader(w, http.StatusInternalServerError)
|
||||
goto oops
|
||||
@ -397,7 +407,7 @@ func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Re
|
||||
} else {
|
||||
status_code = resp.StatusCode
|
||||
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)
|
||||
if err != nil { goto oops }
|
||||
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)
|
||||
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
|
||||
@ -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 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 {
|
||||
ConnId string
|
||||
RouteId string
|
||||
}
|
||||
|
||||
func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||
func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||
var s *Server
|
||||
var status_code int
|
||||
var err error
|
||||
|
||||
s = pxy.s
|
||||
s = pxy.S
|
||||
|
||||
switch pxy.file {
|
||||
case "xterm.js":
|
||||
status_code = write_js_resp_header(w, http.StatusOK)
|
||||
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||
w.Write(xterm_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)
|
||||
case "xterm.css":
|
||||
status_code = WriteCssRespHeader(w, http.StatusOK)
|
||||
@ -483,13 +488,13 @@ 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.
|
||||
// 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")
|
||||
route_id = PORT_ID_MARKER
|
||||
_, err = s.FindServerRouteByIdStr(conn_id, route_id)
|
||||
if err != nil && pxy.s.wpx_foreign_port_proxy_maker != nil {
|
||||
_, err = pxy.s.wpx_foreign_port_proxy_maker("ssh", conn_id)
|
||||
}
|
||||
if err != nil && s.wpx_foreign_port_proxy_maker != nil {
|
||||
_, err = s.wpx_foreign_port_proxy_maker("ssh", conn_id)
|
||||
}
|
||||
} else {
|
||||
conn_id = req.PathValue("conn_id")
|
||||
route_id = req.PathValue("route_id")
|
||||
@ -512,7 +517,8 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
||||
} else {
|
||||
status_code = WriteHtmlRespHeader(w, http.StatusOK)
|
||||
tmpl.Execute(w,
|
||||
&server_proxy_xterm_session_info{
|
||||
&xterm_session_info{
|
||||
Mode: "ssh",
|
||||
ConnId: conn_id,
|
||||
RouteId: route_id,
|
||||
})
|
||||
@ -528,8 +534,17 @@ func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.R
|
||||
case "_forbidden":
|
||||
status_code = WriteEmptyRespHeader(w, http.StatusForbidden)
|
||||
|
||||
default:
|
||||
case "_notfound":
|
||||
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:
|
||||
@ -540,31 +555,20 @@ oops:
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
|
||||
type server_proxy_ssh_ws struct {
|
||||
s *Server
|
||||
type server_pxy_ssh_ws struct {
|
||||
S *Server
|
||||
ws *websocket.Conn
|
||||
id string
|
||||
Id string
|
||||
}
|
||||
|
||||
type json_ssh_ws_event struct {
|
||||
Type string `json:"type"`
|
||||
Data []string `json:"data"`
|
||||
func (pxy *server_pxy_ssh_ws) Identity() string {
|
||||
return pxy.Id
|
||||
}
|
||||
|
||||
// TODO: put this task to sync group.
|
||||
// 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 {
|
||||
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) {
|
||||
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 cc *ssh.ClientConfig
|
||||
var addr *net.TCPAddr
|
||||
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 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{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{ ssh.Password(password) },
|
||||
@ -625,9 +633,10 @@ oops:
|
||||
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 req *http.Request
|
||||
var port_id string
|
||||
var conn_id string
|
||||
var route_id string
|
||||
var r *ServerRoute
|
||||
@ -640,21 +649,28 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
var conn_ready_chan chan bool
|
||||
var connect_ssh_ctx context.Context
|
||||
var connect_ssh_cancel context.CancelFunc
|
||||
var connect_ssh_cancel Atom[context.CancelFunc]
|
||||
var err error
|
||||
|
||||
s = pxy.s
|
||||
s = pxy.S
|
||||
req = ws.Request()
|
||||
conn_ready_chan = make(chan bool, 3)
|
||||
|
||||
port_id = req.PathValue("port_id")
|
||||
conn_id = req.PathValue("conn_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)
|
||||
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
|
||||
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 {
|
||||
pxy.send_ws_data(ws, "error", err.Error())
|
||||
send_ws_data_for_xterm(ws, "error", err.Error())
|
||||
goto done
|
||||
}
|
||||
|
||||
@ -666,7 +682,7 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
|
||||
r = proxy_info_to_server_route(pi)
|
||||
}
|
||||
if err != nil {
|
||||
pxy.send_ws_data(ws, "error", err.Error())
|
||||
send_ws_data_for_xterm(ws, "error", err.Error())
|
||||
goto done
|
||||
}
|
||||
|
||||
@ -679,27 +695,27 @@ func (pxy *server_proxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) {
|
||||
|
||||
conn_ready = <-conn_ready_chan
|
||||
if conn_ready { // connected
|
||||
var buf []byte
|
||||
var buf [2048]byte
|
||||
var n int
|
||||
var err error
|
||||
|
||||
s.stats.ssh_proxy_sessions.Add(1)
|
||||
buf = make([]byte, 2048)
|
||||
for {
|
||||
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
|
||||
}
|
||||
n, err = out.Read(buf[:])
|
||||
if n > 0 {
|
||||
err = pxy.send_ws_data(ws, "iov", string(buf[:n]))
|
||||
if err != nil {
|
||||
s.log.Write(pxy.id, LOG_ERROR, "Failed to send to websocket - %s", err.Error())
|
||||
var err2 error
|
||||
err2 = send_ws_data_for_xterm(ws, "iov", string(buf[:n]))
|
||||
if err2 != nil {
|
||||
s.log.Write(pxy.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(pxy.Id, LOG_ERROR, "[%s] Failed to read from SSH stdout - %s", req.RemoteAddr, err.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
s.stats.ssh_proxy_sessions.Add(-1)
|
||||
}
|
||||
@ -712,16 +728,19 @@ ws_recv_loop:
|
||||
if err != nil { goto done }
|
||||
|
||||
if len(msg) > 0 {
|
||||
var ev json_ssh_ws_event
|
||||
var ev json_xterm_ws_event
|
||||
err = json.Unmarshal(msg, &ev)
|
||||
if err == nil {
|
||||
switch ev.Type {
|
||||
case "open":
|
||||
if sess == nil && len(ev.Data) == 2 {
|
||||
var cancel context.CancelFunc
|
||||
|
||||
username = string(ev.Data[0])
|
||||
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)
|
||||
go func() {
|
||||
@ -730,26 +749,27 @@ ws_recv_loop:
|
||||
defer wg.Done()
|
||||
c, sess, in, out, err = pxy.connect_ssh(connect_ssh_ctx, username, password, r)
|
||||
if err != nil {
|
||||
s.log.Write(pxy.id, LOG_ERROR, "failed to connect ssh - %s", err.Error())
|
||||
pxy.send_ws_data(ws, "error", err.Error())
|
||||
s.log.Write(pxy.Id, LOG_ERROR, "[%s] Failed to connect ssh - %s", req.RemoteAddr, err.Error())
|
||||
send_ws_data_for_xterm(ws, "error", err.Error())
|
||||
ws.Close() // dirty way to flag out the error
|
||||
} else {
|
||||
err = pxy.send_ws_data(ws, "status", "opened")
|
||||
err = send_ws_data_for_xterm(ws, "status", "opened")
|
||||
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
|
||||
} else {
|
||||
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Opened SSH session", req.RemoteAddr)
|
||||
conn_ready_chan <- true
|
||||
}
|
||||
}
|
||||
connect_ssh_cancel()
|
||||
connect_ssh_cancel = nil
|
||||
(connect_ssh_cancel.Get())()
|
||||
connect_ssh_cancel.Set(nil) // @@@ use atomic
|
||||
}()
|
||||
}
|
||||
|
||||
case "close":
|
||||
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() }
|
||||
break ws_recv_loop
|
||||
|
||||
@ -768,7 +788,7 @@ ws_recv_loop:
|
||||
rows, _ = strconv.Atoi(ev.Data[0])
|
||||
cols, _ = strconv.Atoi(ev.Data[1])
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -777,7 +797,7 @@ ws_recv_loop:
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
@ -787,9 +807,7 @@ done:
|
||||
if sess != nil { sess.Close() }
|
||||
if c != nil { c.Close() }
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
s.log.Write(pxy.id, LOG_ERROR, "[%s] %s %s - %s", req.RemoteAddr, req.Method, req.URL.String(), err.Error())
|
||||
} else {
|
||||
s.log.Write(pxy.id, LOG_DEBUG, "[%s] %s %s - ended", req.RemoteAddr, req.Method, req.URL.String())
|
||||
}
|
||||
s.log.Write(pxy.Id, LOG_DEBUG, "[%s] Ended SSH Session", req.RemoteAddr)
|
||||
|
||||
return http.StatusOK, err
|
||||
}
|
330
server-rpx.go
Normal file
330
server-rpx.go
Normal 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
|
||||
}
|
@ -68,7 +68,7 @@ func (t *Transformer) Transform(dst []byte, src []byte, at_eof bool) (int, int,
|
||||
err = transform.ErrShortSrc
|
||||
|
||||
done:
|
||||
return ndst, nsrc, err
|
||||
return ndst, nsrc, err
|
||||
}
|
||||
|
||||
func (t *Transformer) copy_all(dst []byte, src []byte) (int, error) {
|
||||
|
97
xterm.html
97
xterm.html
@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Terminal</title>
|
||||
<link rel="stylesheet" href="/_ssh/xterm.css" />
|
||||
<link rel="stylesheet" href="xterm.css" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
@ -86,10 +86,11 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script src="/_ssh/xterm.js"></script>
|
||||
<script src="/_ssh/xterm-addon-fit.js"></script>
|
||||
<script src="xterm.js"></script>
|
||||
<script src="xterm-addon-fit.js"></script>
|
||||
|
||||
<script>
|
||||
const xt_mode = '{{ .Mode }}';
|
||||
const conn_id = '{{ .ConnId }}';
|
||||
const route_id = '{{ .RouteId }}';
|
||||
|
||||
@ -99,12 +100,28 @@ window.onload = function(event) {
|
||||
const terminal_status = document.getElementById('terminal-status');
|
||||
const terminal_errmsg = document.getElementById('terminal-errmsg');
|
||||
const terminal_view_container = document.getElementById('terminal-view-container');
|
||||
const terminal_connect = document.getElementById('terminal-connect');
|
||||
const terminal_disconnect = document.getElementById('terminal-disconnect');
|
||||
const login_container = document.getElementById('login-container');
|
||||
const login_form_title = document.getElementById('login-form-title');
|
||||
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 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({
|
||||
lineHeight: 1.2,
|
||||
@ -133,11 +150,19 @@ window.onload = function(event) {
|
||||
|
||||
let fetch_session_info = async function() {
|
||||
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 {
|
||||
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()
|
||||
if ('client-peer-name' in route) {
|
||||
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) {
|
||||
if (visible) fetch_session_info();
|
||||
if (visible && xt_mode == 'ssh') fetch_session_info();
|
||||
login_container.style.visibility = (visible? 'visible': 'hidden');
|
||||
terminal_disconnect.style.visibility = (visible? 'hidden': 'visible');
|
||||
if (visible) username_field.focus();
|
||||
else term.focus();
|
||||
if (visible) {
|
||||
if (xt_mode == 'ssh') username_field.focus();
|
||||
else terminal_connect.focus();
|
||||
} else {
|
||||
term.focus();
|
||||
}
|
||||
}
|
||||
|
||||
toggle_login_form(true);
|
||||
@ -166,12 +195,27 @@ window.onload = function(event) {
|
||||
event.preventDefault();
|
||||
toggle_login_form(false)
|
||||
|
||||
const username = username_field.value.trim();
|
||||
const password = password_field.value.trim();
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
||||
let prefix = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
let url = prefix + window.location.host+ `/_ssh-ws/${conn_id}/${route_id}`;
|
||||
const socket = new WebSocket(url)
|
||||
if (xt_mode == 'ssh') {
|
||||
username = username_field.value.trim();
|
||||
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';
|
||||
|
||||
set_terminal_status('Connecting...', '');
|
||||
@ -217,7 +261,8 @@ window.onload = function(event) {
|
||||
};
|
||||
|
||||
socket.onerror = function(event) {
|
||||
set_terminal_status('Disconnected', event);
|
||||
//set_terminal_status('Disconnected', event);
|
||||
set_terminal_status('Disconnected', '');
|
||||
toggle_login_form(true)
|
||||
window.onresize = adjust_terminal_size_unconnected;
|
||||
};
|
||||
@ -249,15 +294,21 @@ window.onload = function(event) {
|
||||
<div id="login-form-container">
|
||||
<div id="login-form-title"></div>
|
||||
<form id="login-form">
|
||||
<label>
|
||||
Username: <input type="text" id="username" required />
|
||||
</label>
|
||||
<br /><br />
|
||||
<label>
|
||||
Password: <input type="password" id="password" required />
|
||||
</label>
|
||||
<br /><br />
|
||||
<button type="submit">Connect</button>
|
||||
<div id="login-ssh-part" style="display: none;">
|
||||
<label>
|
||||
Username: <input type="text" id="username" disabled required />
|
||||
</label>
|
||||
<br /><br />
|
||||
<label>
|
||||
Password: <input type="password" id="password" disabled required />
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user