Compare commits
18 Commits
e1c47c041d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0aa89c83d5 | |||
| f8f1b79fd0 | |||
| 844bbfee31 | |||
| 0d966108ee | |||
| 32c0bc1940 | |||
| ce76d9c3cc | |||
| 555b9a3a80 | |||
| c4a0342067 | |||
| 76c1e2135e | |||
| 6f8658a0e8 | |||
| a9aa55a526 | |||
| e832257563 | |||
| eabc844ef3 | |||
| b7031c1630 | |||
| 5595c3813f | |||
| 134ef7feec | |||
| 0c8fb18a7a | |||
| 14b5b82881 |
@@ -26,6 +26,7 @@ SRCS=\
|
|||||||
jwt.go \
|
jwt.go \
|
||||||
packet.go \
|
packet.go \
|
||||||
pty.go \
|
pty.go \
|
||||||
|
rsa-aes.go \
|
||||||
server.go \
|
server.go \
|
||||||
server-ctl.go \
|
server-ctl.go \
|
||||||
server-cts-rpty.go \
|
server-cts-rpty.go \
|
||||||
@@ -42,9 +43,8 @@ SRCS=\
|
|||||||
transform.go \
|
transform.go \
|
||||||
|
|
||||||
DATA = \
|
DATA = \
|
||||||
xterm.css \
|
xterm-plain.js \
|
||||||
xterm.js \
|
xterm-plain.html \
|
||||||
xterm-addon-fit.js \
|
|
||||||
xterm.html
|
xterm.html
|
||||||
|
|
||||||
CMD_DATA=\
|
CMD_DATA=\
|
||||||
@@ -84,15 +84,11 @@ hodu_grpc.pb.go: hodu.proto
|
|||||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||||
hodu.proto
|
hodu.proto
|
||||||
|
|
||||||
xterm.js:
|
xterm.html: xterm-plain.html xterm-plain.js vite.config.js package.json
|
||||||
curl -L -o "$@" https://cdn.jsdelivr.net/npm/@xterm/xterm/lib/xterm.min.js
|
rm -rf .vite-xterm
|
||||||
|
npm run build:xterm
|
||||||
xterm-addon-fit.js:
|
cp .vite-xterm/xterm-plain.html "$@"
|
||||||
curl -L -o "$@" https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.min.js
|
rm -rf .vite-xterm
|
||||||
|
|
||||||
xterm.css:
|
|
||||||
curl -L -o "$@" https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.min.css
|
|
||||||
sed -r -i 's|^/\*# sourceMappingURL=/.+ \*/$$||g' "$@"
|
|
||||||
|
|
||||||
cmd/tls.crt:
|
cmd/tls.crt:
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout cmd/tls.key -out cmd/tls.crt -sha256 -days 36500 -nodes -subj "/CN=$(NAME)" --addext "subjectAltName=DNS:$(NAME),IP:127.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"
|
||||||
@@ -103,4 +99,11 @@ cmd/tls.key:
|
|||||||
cmd/rsa.key:
|
cmd/rsa.key:
|
||||||
openssl genrsa -traditional -out cmd/rsa.key 2048
|
openssl genrsa -traditional -out cmd/rsa.key 2048
|
||||||
|
|
||||||
.PHONY: all clean test
|
mtls-client-cert-for-test:
|
||||||
|
## you can use this recipe to generate certificate/key files for mtls testing against
|
||||||
|
## the built-in certificate used as trusted ca.
|
||||||
|
openssl genrsa -out mtls-client.key 2048
|
||||||
|
openssl req -new -key mtls-client.key -out mtls-client.csr -subj "/CN=mtls-client"
|
||||||
|
openssl x509 -req -in mtls-client.csr -CA cmd/tls.crt -CAkey cmd/tls.key -CAcreateserial -out mtls-client.crt -days 365
|
||||||
|
|
||||||
|
.PHONY: all clean test mtls-client-cert-for-test
|
||||||
|
|||||||
@@ -21,6 +21,32 @@ On the client-side:
|
|||||||
- curl -v -H 'Host: tratra' http://127.0.0.1:9996/hello/world
|
- curl -v -H 'Host: tratra' http://127.0.0.1:9996/hello/world
|
||||||
- this hits the port 1212 on the client side
|
- this hits the port 1212 on the client side
|
||||||
|
|
||||||
|
- If you need to enable protection, you need a rsa key pair.
|
||||||
|
- openssl genrsa -traditional -out private.pem 2048
|
||||||
|
- openssl rsa -pubout -in private.pem -out public.pem
|
||||||
|
|
||||||
|
- You should generate a encrypted client token and use it.
|
||||||
|
- python rsa-aes-256-gcm.py encipher public.pem client-token-value
|
||||||
|
- php rsa-aes-256-gcm.php encipher public.pem client-token-value
|
||||||
|
|
||||||
|
Given the following configuration snippet:
|
||||||
|
```
|
||||||
|
rpx:
|
||||||
|
client-token:
|
||||||
|
attr-name: X-Client-Token
|
||||||
|
regex:
|
||||||
|
submatch-index: 1
|
||||||
|
protection: rsa-aes-256-gcm
|
||||||
|
token-rsa-key-text:
|
||||||
|
token-rsa-key-file: ./private.pem
|
||||||
|
```
|
||||||
|
The http/https caller can add the `X-Client-Token` header in the request as generated by the rsa-aes-256-gcm utility.
|
||||||
|
|
||||||
|
The format of the protected token is:
|
||||||
|
- ciphertext = b64url(token)|iat|exp
|
||||||
|
- encrypted-aes-key = randomly generated key encrypted with a rsa public key
|
||||||
|
- protected-token = b64url(encrypted-aes-key).b64url(nonce).b64url(ciphertext)
|
||||||
|
|
||||||
### WPX
|
### WPX
|
||||||
- On the client side
|
- On the client side
|
||||||
- python -m http.server 1212
|
- python -m http.server 1212
|
||||||
|
|||||||
+5
-21
@@ -197,7 +197,7 @@ func (ctl *client_ctl) Cors(req *http.Request) bool {
|
|||||||
|
|
||||||
func (ctl *client_ctl) Authenticate(req *http.Request) (int, string) {
|
func (ctl *client_ctl) Authenticate(req *http.Request) (int, string) {
|
||||||
if ctl.c.ctl_auth == nil { return http.StatusOK, "" }
|
if ctl.c.ctl_auth == nil { return http.StatusOK, "" }
|
||||||
return ctl.c.ctl_auth.Authenticate(req)
|
return ctl.c.ctl_auth.Authenticate(req, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@@ -229,7 +229,7 @@ func (ctl *client_ctl_token) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
claim.IssuedAt = now.Unix()
|
claim.IssuedAt = now.Unix()
|
||||||
claim.ExpiresAt = now.Add(c.ctl_auth.TokenTtl).Unix()
|
claim.ExpiresAt = now.Add(c.ctl_auth.TokenTtl).Unix()
|
||||||
jwt = NewJWT(c.ctl_auth.TokenRsaKey, &claim)
|
jwt = NewJWT(c.ctl_auth.TokenRsaKey, &claim)
|
||||||
tok, err = jwt.SignRS512()
|
tok, err = jwt.SignRS256()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusInternalServerError)
|
status_code = WriteJsonRespHeader(w, http.StatusInternalServerError)
|
||||||
je.Encode(JsonErrmsg{Text: err.Error()})
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
@@ -237,7 +237,7 @@ func (ctl *client_ctl_token) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
||||||
err = je.Encode(json_out_token{ AccessToken: tok }) // TODO: refresh token
|
err = je.Encode(json_out_auth_token{ AccessToken: tok }) // TODO: refresh token
|
||||||
if err != nil { goto oops }
|
if err != nil { goto oops }
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1179,24 +1179,8 @@ func (ctl *client_ctl_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
// handle authentication using the first message.
|
// handle authentication using the first message.
|
||||||
// end this task if authentication fails.
|
// end this task if authentication fails.
|
||||||
if !ctl.noauth && c.ctl_auth != nil {
|
if !ctl.noauth && c.ctl_auth != nil {
|
||||||
var req *http.Request
|
status_code, _ = c.ctl_auth.Authenticate(ws.Request(), "access-token")
|
||||||
|
if status_code != http.StatusOK { goto done }
|
||||||
req = ws.Request()
|
|
||||||
if req.Header.Get("Authorization") == "" {
|
|
||||||
var token string
|
|
||||||
token = req.FormValue("token")
|
|
||||||
if token != "" {
|
|
||||||
// websocket doesn't actual have extra headers except a few fixed
|
|
||||||
// ones. add "Authorization" header from the query paramerer and
|
|
||||||
// compose a fake header to reuse the same Authentication() function
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status_code, _ = c.ctl_auth.Authenticate(req)
|
|
||||||
if status_code != http.StatusOK {
|
|
||||||
goto done
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sbsc, err = c.bulletin.Subscribe("")
|
sbsc, err = c.bulletin.Subscribe("")
|
||||||
|
|||||||
+35
-5
@@ -3,6 +3,7 @@ package hodu
|
|||||||
import "encoding/base64"
|
import "encoding/base64"
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
import "errors"
|
import "errors"
|
||||||
|
import "fmt"
|
||||||
import "io"
|
import "io"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
import "os"
|
import "os"
|
||||||
@@ -50,6 +51,18 @@ func (pty *client_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
|
|
||||||
c = pty.C
|
c = pty.C
|
||||||
req = ws.Request()
|
req = ws.Request()
|
||||||
|
|
||||||
|
// handle authentication using the first message.
|
||||||
|
// end this task if authentication fails.
|
||||||
|
if c.ctl_auth != nil {
|
||||||
|
var status_code int
|
||||||
|
status_code, _ = c.ctl_auth.Authenticate(req, "access-token")
|
||||||
|
if status_code != http.StatusOK {
|
||||||
|
ws.Close()
|
||||||
|
return status_code, fmt.Errorf("failed to authenticate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn_ready_chan = make(chan bool, 3)
|
conn_ready_chan = make(chan bool, 3)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -71,8 +84,8 @@ func (pty *client_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
poll_fds = []unix.PollFd{
|
poll_fds = []unix.PollFd{
|
||||||
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
|
|
||||||
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
|
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
|
||||||
|
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.stats.pty_sessions.Add(1)
|
c.stats.pty_sessions.Add(1)
|
||||||
@@ -87,8 +100,8 @@ func (pty *client_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
out_revents = poll_fds[0].Revents
|
sig_revents = poll_fds[0].Revents
|
||||||
sig_revents = poll_fds[1].Revents
|
out_revents = poll_fds[1].Revents
|
||||||
|
|
||||||
if (out_revents & unix.POLLIN) != 0 {
|
if (out_revents & unix.POLLIN) != 0 {
|
||||||
n, err = out.Read(buf[:])
|
n, err = out.Read(buf[:])
|
||||||
@@ -262,6 +275,21 @@ done:
|
|||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
|
|
||||||
|
func (pty *client_pty_xterm_file) Authenticate(req *http.Request) (int, string) {
|
||||||
|
if pty.c.ctl_auth != nil && pty.file == "xterm.html" {
|
||||||
|
// this is not a real api call. but at least for xterm.html,
|
||||||
|
// i don't bypass authentication and and in addition,
|
||||||
|
// i check the value of the "access-token" parameter for
|
||||||
|
// jwt authentication if it exists
|
||||||
|
return pty.c.ctl_auth.Authenticate(req, "access-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// you can download other files without authentication
|
||||||
|
// even if authentication is enabled. but this part must not be reached
|
||||||
|
// because xterm.html is the only meanful resource as of now
|
||||||
|
return http.StatusOK, ""
|
||||||
|
}
|
||||||
|
|
||||||
func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||||
var c *Client
|
var c *Client
|
||||||
var status_code int
|
var status_code int
|
||||||
@@ -270,6 +298,7 @@ func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
c = pty.c
|
c = pty.c
|
||||||
|
|
||||||
switch pty.file {
|
switch pty.file {
|
||||||
|
/*
|
||||||
case "xterm.js":
|
case "xterm.js":
|
||||||
status_code = WriteJsRespHeader(w, http.StatusOK)
|
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_js)
|
w.Write(xterm_js)
|
||||||
@@ -282,10 +311,11 @@ func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
case "xterm.css":
|
case "xterm.css":
|
||||||
status_code = WriteCssRespHeader(w, http.StatusOK)
|
status_code = WriteCssRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_css)
|
w.Write(xterm_css)
|
||||||
|
*/
|
||||||
case "xterm.html":
|
case "xterm.html":
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
|
|
||||||
tmpl = template.New("")
|
tmpl = template.New("").Delims("{{@@", "@@}}")
|
||||||
if c.xterm_html != "" {
|
if c.xterm_html != "" {
|
||||||
_, err = tmpl.Parse(c.xterm_html)
|
_, err = tmpl.Parse(c.xterm_html)
|
||||||
} else {
|
} else {
|
||||||
@@ -314,7 +344,7 @@ func (pty *client_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
default:
|
default:
|
||||||
if strings.HasPrefix(pty.file, "_redir:") {
|
if strings.HasPrefix(pty.file, "_redir:") {
|
||||||
status_code = http.StatusMovedPermanently
|
status_code = http.StatusMovedPermanently
|
||||||
w.Header().Set("Location", pty.file[7:])
|
w.Header().Set("Location", append_raw_query(pty.file[7:], req))
|
||||||
w.WriteHeader(status_code)
|
w.WriteHeader(status_code)
|
||||||
} else {
|
} else {
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
|
|||||||
@@ -1710,6 +1710,8 @@ func (c *Client) WrapHttpHandler(handler ClientHttpHandler) http.Handler {
|
|||||||
var err error
|
var err error
|
||||||
var start_time time.Time
|
var start_time time.Time
|
||||||
var time_taken time.Duration
|
var time_taken time.Duration
|
||||||
|
var newctx context.Context
|
||||||
|
var xinfo HttpHandlerExtraInfo
|
||||||
|
|
||||||
// this deferred function is to overcome the recovering implemenation
|
// this deferred function is to overcome the recovering implemenation
|
||||||
// from panic done in go's http server. in that implemenation, panic
|
// from panic done in go's http server. in that implemenation, panic
|
||||||
@@ -1723,6 +1725,9 @@ func (c *Client) WrapHttpHandler(handler ClientHttpHandler) http.Handler {
|
|||||||
|
|
||||||
start_time = time.Now()
|
start_time = time.Now()
|
||||||
|
|
||||||
|
newctx = context.WithValue(req.Context(), http_handler_extra_info_key, &xinfo)
|
||||||
|
req = req.WithContext(newctx)
|
||||||
|
|
||||||
if handler.Cors(req) {
|
if handler.Cors(req) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
@@ -1750,10 +1755,12 @@ func (c *Client) WrapHttpHandler(handler ClientHttpHandler) http.Handler {
|
|||||||
time_taken = time.Since(start_time) //time.Now().Sub(start_time)
|
time_taken = time.Since(start_time) //time.Now().Sub(start_time)
|
||||||
|
|
||||||
if status_code > 0 {
|
if status_code > 0 {
|
||||||
|
var id string = handler.Identity()
|
||||||
|
if xinfo.extra_id != "" { id = id + "/" + xinfo.extra_id }
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log.Write(handler.Identity(), LOG_INFO, "[%s] %s %s %d %.9f - %s", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds(), err.Error())
|
c.log.Write(id, LOG_INFO, "[%s] %s %s %d %.9f - %s", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds(), err.Error())
|
||||||
} else {
|
} else {
|
||||||
c.log.Write(handler.Identity(), LOG_INFO, "[%s] %s %s %d %.9f", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds())
|
c.log.Write(id, LOG_INFO, "[%s] %s %s %d %.9f", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1895,28 +1902,11 @@ func NewClient(ctx context.Context, name string, logger Logger, cfg *ClientConfi
|
|||||||
|
|
||||||
c.ctl_mux.Handle("/_pty/ws",
|
c.ctl_mux.Handle("/_pty/ws",
|
||||||
c.SafeWrapWebsocketHandler(c.WrapWebsocketHandler(&client_pty_ws{C: &c, Id: HS_ID_CTL})))
|
c.SafeWrapWebsocketHandler(c.WrapWebsocketHandler(&client_pty_ws{C: &c, Id: HS_ID_CTL})))
|
||||||
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm.js",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.js"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm.js/",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm-addon-fit.js",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm-addon-fit.js"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm-addon-fit.js/",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm-addon-unicode11.js",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm-addon-unicode11.js"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm-addon-unicode11.js/",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm.css",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.css"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm.css/",
|
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
c.ctl_mux.Handle("/_pty/xterm.html",
|
c.ctl_mux.Handle("/_pty/xterm.html",
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.html"}))
|
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "xterm.html"}))
|
||||||
c.ctl_mux.Handle("/_pty/xterm.html/", // without this forbidden, /_pty/xterm.js/ access resulted in xterm.html.
|
c.ctl_mux.Handle("/_pty/xterm.html/",
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
c.ctl_mux.Handle("/_pty/",
|
c.ctl_mux.Handle("/_pty/{$}",
|
||||||
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
c.WrapHttpHandler(&client_pty_xterm_file{client_ctl: client_ctl{c: &c, id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
||||||
|
|
||||||
c.ctl_addr = make([]string, len(cfg.CtlAddrs))
|
c.ctl_addr = make([]string, len(cfg.CtlAddrs))
|
||||||
|
|||||||
+164
-30
@@ -9,6 +9,7 @@ import "fmt"
|
|||||||
import "hodu"
|
import "hodu"
|
||||||
import "net/netip"
|
import "net/netip"
|
||||||
import "os"
|
import "os"
|
||||||
|
import "regexp"
|
||||||
import "strings"
|
import "strings"
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@@ -16,6 +17,23 @@ import "time"
|
|||||||
import yaml "github.com/goccy/go-yaml"
|
import yaml "github.com/goccy/go-yaml"
|
||||||
|
|
||||||
|
|
||||||
|
type ServerRptyConfig struct {
|
||||||
|
ClientToken struct {
|
||||||
|
Protection string `yaml:"protection"`
|
||||||
|
TokenRsaKeyText string `yaml:"token-rsa-key-text"`
|
||||||
|
TokenRsaKeyFile string `yaml:"token-rsa-key-file"`
|
||||||
|
TokenTtl string `yaml:"token-ttl"`
|
||||||
|
} `yaml:"client-token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerTLSSNIConfig struct {
|
||||||
|
Name string `yaml:"name-regex"`
|
||||||
|
CertFile string `yaml:"cert-file"`
|
||||||
|
KeyFile string `yaml:"key-file"`
|
||||||
|
CertText string `yaml:"cert-text"`
|
||||||
|
KeyText string `yaml:"key-text"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerTLSConfig struct {
|
type ServerTLSConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
CertFile string `yaml:"cert-file"`
|
CertFile string `yaml:"cert-file"`
|
||||||
@@ -32,9 +50,13 @@ type ServerTLSConfig struct {
|
|||||||
//MaxVersion TLSVersion `yaml:"max-version"`
|
//MaxVersion TLSVersion `yaml:"max-version"`
|
||||||
//PreferServerCipherSuites bool `yaml:"prefer-server-cipher-suites"`
|
//PreferServerCipherSuites bool `yaml:"prefer-server-cipher-suites"`
|
||||||
//ClientAllowedSans []string `yaml:"client-allowed-sans"`
|
//ClientAllowedSans []string `yaml:"client-allowed-sans"`
|
||||||
|
|
||||||
|
SNIs []ServerTLSSNIConfig `yaml:"snis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientTLSConfig struct {
|
type ClientTLSConfig struct {
|
||||||
|
// The Enabled field doesnt indicate using or not using tls.
|
||||||
|
// It expresses the rest of items are applied or not in tls configuration.
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
CertFile string `yaml:"cert-file"`
|
CertFile string `yaml:"cert-file"`
|
||||||
KeyFile string `yaml:"key-file"`
|
KeyFile string `yaml:"key-file"`
|
||||||
@@ -69,6 +91,11 @@ type CTLServiceConfig struct {
|
|||||||
Auth HttpAuthConfig `yaml:"auth"`
|
Auth HttpAuthConfig `yaml:"auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ECTServiceConfig struct {
|
||||||
|
Addrs []string `yaml:"addresses"`
|
||||||
|
Auth HttpAuthConfig `yaml:"auth"`
|
||||||
|
}
|
||||||
|
|
||||||
type RPXServiceConfig struct {
|
type RPXServiceConfig struct {
|
||||||
Addrs []string `yaml:"addresses"`
|
Addrs []string `yaml:"addresses"`
|
||||||
}
|
}
|
||||||
@@ -77,14 +104,21 @@ type RPXClientTokenConfig struct {
|
|||||||
AttrName string `yaml:"attr-name"`
|
AttrName string `yaml:"attr-name"`
|
||||||
Regex string `yaml:"regex"`
|
Regex string `yaml:"regex"`
|
||||||
SubmatchIndex int `yaml:"submatch-index"`
|
SubmatchIndex int `yaml:"submatch-index"`
|
||||||
|
|
||||||
|
Protection string `yaml:"protection"`
|
||||||
|
TokenRsaKeyText string `yaml:"token-rsa-key-text"`
|
||||||
|
TokenRsaKeyFile string `yaml:"token-rsa-key-file"`
|
||||||
|
TokenTtl string `yaml:"token-ttl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PXYServiceConfig struct {
|
type PXYServiceConfig struct {
|
||||||
Addrs []string `yaml:"addresses"`
|
Addrs []string `yaml:"addresses"`
|
||||||
|
Auth HttpAuthConfig `yaml:"auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WPXServiceConfig struct {
|
type WPXServiceConfig struct {
|
||||||
Addrs []string `yaml:"addresses"`
|
Addrs []string `yaml:"addresses"`
|
||||||
|
Auth HttpAuthConfig `yaml:"auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPCServiceConfig struct { // rpc server-side configuration
|
type RPCServiceConfig struct { // rpc server-side configuration
|
||||||
@@ -147,8 +181,14 @@ type ServerConfig struct {
|
|||||||
CTL struct {
|
CTL struct {
|
||||||
Service CTLServiceConfig `yaml:"service"`
|
Service CTLServiceConfig `yaml:"service"`
|
||||||
TLS ServerTLSConfig `yaml:"tls"`
|
TLS ServerTLSConfig `yaml:"tls"`
|
||||||
|
Rpty ServerRptyConfig `yaml:"rpty"`
|
||||||
} `yaml:"ctl"`
|
} `yaml:"ctl"`
|
||||||
|
|
||||||
|
ECT struct {
|
||||||
|
Service ECTServiceConfig `yaml:"service"`
|
||||||
|
TLS ServerTLSConfig `yaml:"tls"`
|
||||||
|
} `yaml:"ect"`
|
||||||
|
|
||||||
RPX struct {
|
RPX struct {
|
||||||
Service RPXServiceConfig `yaml:"service"`
|
Service RPXServiceConfig `yaml:"service"`
|
||||||
TLS ServerTLSConfig `yaml:"tls"`
|
TLS ServerTLSConfig `yaml:"tls"`
|
||||||
@@ -159,7 +199,10 @@ type ServerConfig struct {
|
|||||||
Service PXYServiceConfig `yaml:"service"`
|
Service PXYServiceConfig `yaml:"service"`
|
||||||
TLS ServerTLSConfig `yaml:"tls"`
|
TLS ServerTLSConfig `yaml:"tls"`
|
||||||
Target struct {
|
Target struct {
|
||||||
TLS ClientTLSConfig `yaml:"tls"`
|
// TODO: This will have to be extended to be an array
|
||||||
|
// of configurations to cater for different targets
|
||||||
|
// It needs a name or a name pattern field to match the target.
|
||||||
|
TLS ClientTLSConfig `yaml:"tls"`
|
||||||
} `yaml:"target"`
|
} `yaml:"target"`
|
||||||
} `yaml:"pxy"`
|
} `yaml:"pxy"`
|
||||||
|
|
||||||
@@ -276,15 +319,75 @@ func log_strings_to_mask(str []string) hodu.LogMask {
|
|||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
type ServerSNICert struct {
|
||||||
|
name *regexp.Regexp
|
||||||
|
cert tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostname_wildcard_match(wildcard string, name string) bool {
|
||||||
|
var suffix_start int
|
||||||
|
var suffix_len int
|
||||||
|
var prefix_end int
|
||||||
|
var i int
|
||||||
|
|
||||||
|
// must start with "*."
|
||||||
|
if len(wildcard) < 3 || wildcard[0] != '*' || wildcard[1] != '.' { return false }
|
||||||
|
|
||||||
|
// suffix is everything after "*."
|
||||||
|
suffix_start = 1 // points to "."
|
||||||
|
suffix_len = len(wildcard) - suffix_start
|
||||||
|
|
||||||
|
// name must be longer than suffix (so there's at least one label)
|
||||||
|
if len(name) <= suffix_len { return false }
|
||||||
|
|
||||||
|
// check suffix match
|
||||||
|
if name[len(name)-suffix_len:] != wildcard[suffix_start:] { return false }
|
||||||
|
|
||||||
|
// check there is exactly one label before suffix
|
||||||
|
prefix_end = len(name) - suffix_len
|
||||||
|
if prefix_end == 0 { return false }
|
||||||
|
|
||||||
|
// ensure no '.' in the prefix (only one label)
|
||||||
|
for i = 0; i < prefix_end; i++ {
|
||||||
|
if name[i] == '.' { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
|
func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
|
||||||
var tlscfg *tls.Config
|
var tlscfg *tls.Config
|
||||||
|
|
||||||
if cfg.Enabled {
|
if cfg.Enabled {
|
||||||
var cert tls.Certificate
|
var cert tls.Certificate
|
||||||
var cert_pool *x509.CertPool
|
var cert_pool *x509.CertPool
|
||||||
|
var sni_certs []ServerSNICert
|
||||||
|
var i int
|
||||||
var ok bool
|
var ok bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
for i = range cfg.SNIs {
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
|
||||||
|
if cfg.SNIs[i].CertText != "" && cfg.SNIs[i].KeyText != "" {
|
||||||
|
cert, err = tls.X509KeyPair([]byte(cfg.SNIs[i].CertText), []byte(cfg.SNIs[i].KeyText))
|
||||||
|
} else if cfg.SNIs[i].CertFile != "" && cfg.SNIs[i].KeyFile != "" {
|
||||||
|
cert, err = tls.LoadX509KeyPair(cfg.SNIs[i].CertFile, cfg.SNIs[i].KeyFile)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load sni key pair - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
regex, err = regexp.Compile(cfg.SNIs[i].Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to compile sni name-regex - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
sni_certs = append(sni_certs, ServerSNICert{name: regex, cert: cert});
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.CertText != "" && cfg.KeyText != "" {
|
if cfg.CertText != "" && cfg.KeyText != "" {
|
||||||
cert, err = tls.X509KeyPair([]byte(cfg.CertText), []byte(cfg.KeyText))
|
cert, err = tls.X509KeyPair([]byte(cfg.CertText), []byte(cfg.KeyText))
|
||||||
} else if cfg.CertFile != "" && cfg.KeyFile != "" {
|
} else if cfg.CertFile != "" && cfg.KeyFile != "" {
|
||||||
@@ -301,7 +404,7 @@ func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
|
|||||||
if cfg.ClientCACertText != "" {
|
if cfg.ClientCACertText != "" {
|
||||||
ok = cert_pool.AppendCertsFromPEM([]byte(cfg.ClientCACertText))
|
ok = cert_pool.AppendCertsFromPEM([]byte(cfg.ClientCACertText))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to append certificate to pool")
|
return nil, fmt.Errorf("failed to append configured certificate text to pool")
|
||||||
}
|
}
|
||||||
} else if cfg.ClientCACertFile != "" {
|
} else if cfg.ClientCACertFile != "" {
|
||||||
var text []byte
|
var text []byte
|
||||||
@@ -311,22 +414,40 @@ func make_tls_server_config(cfg *ServerTLSConfig) (*tls.Config, error) {
|
|||||||
}
|
}
|
||||||
ok = cert_pool.AppendCertsFromPEM(text)
|
ok = cert_pool.AppendCertsFromPEM(text)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to append certificate to pool")
|
return nil, fmt.Errorf("failed to append configured certificate file to pool")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// if the client ca cert is not specified, use the bundled tls cert
|
||||||
|
// as a trusted ca cert.
|
||||||
ok = cert_pool.AppendCertsFromPEM(hodu_tls_cert_text)
|
ok = cert_pool.AppendCertsFromPEM(hodu_tls_cert_text)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to append certificate to pool")
|
return nil, fmt.Errorf("failed to append builtin certificate to pool")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tlscfg = &tls.Config{
|
tlscfg = &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
// If multiple certificates are configured, we may have to implement GetCertificate
|
|
||||||
// GetCertificate: func (chi *tls.ClientHelloInfo) (*Certificate, error) { return cert, nil }
|
|
||||||
ClientAuth: tls_string_to_client_auth_type(cfg.ClientAuthType),
|
ClientAuth: tls_string_to_client_auth_type(cfg.ClientAuthType),
|
||||||
ClientCAs: cert_pool, // trusted CA certs for client certificate verification
|
ClientCAs: cert_pool, // trusted CA certs for client certificate verification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (len(sni_certs) > 0) {
|
||||||
|
tlscfg.GetCertificate = func (chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
var server_name string
|
||||||
|
var x int
|
||||||
|
|
||||||
|
server_name = strings.TrimSuffix(strings.ToLower(chi.ServerName), ".")
|
||||||
|
if server_name == "" { return nil, nil }
|
||||||
|
|
||||||
|
for x = range sni_certs {
|
||||||
|
if sni_certs[x].name.MatchString(server_name) {
|
||||||
|
return &sni_certs[x].cert, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tlscfg, nil
|
return tlscfg, nil
|
||||||
@@ -355,7 +476,8 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to load key pair - %s", err.Error())
|
return nil, fmt.Errorf("failed to load key pair - %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
cert_pool = x509.NewCertPool()
|
cert_pool, err = x509.SystemCertPool()
|
||||||
|
if err != nil { cert_pool = x509.NewCertPool() }
|
||||||
if cfg.ServerCACertText != "" {
|
if cfg.ServerCACertText != "" {
|
||||||
ok = cert_pool.AppendCertsFromPEM([]byte(cfg.ServerCACertText))
|
ok = cert_pool.AppendCertsFromPEM([]byte(cfg.ServerCACertText))
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -392,15 +514,44 @@ func make_tls_client_config(cfg *ClientTLSConfig) (*tls.Config, error) {
|
|||||||
return tlscfg, nil
|
return tlscfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
func make_rsa_private_key_config(key_text string, key_file string, default_key_text []byte, key_name string) (*rsa.PrivateKey, error) {
|
||||||
|
var rsa_key_text []byte
|
||||||
|
var rk *rsa.PrivateKey
|
||||||
|
var pb *pem.Block
|
||||||
|
var b []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if key_text == "" && key_file != "" {
|
||||||
|
rsa_key_text, err = os.ReadFile(key_file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read %s - %s", key_file, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rsa_key_text) == 0 { rsa_key_text = []byte(key_text) }
|
||||||
|
if len(rsa_key_text) == 0 { rsa_key_text = default_key_text }
|
||||||
|
if len(rsa_key_text) == 0 { return nil, nil }
|
||||||
|
|
||||||
|
pb, b = pem.Decode(rsa_key_text)
|
||||||
|
if pb == nil || len(b) > 0 {
|
||||||
|
return nil, fmt.Errorf("invalid %s text %.32s... - no block or too many blocks", key_name, string(rsa_key_text))
|
||||||
|
}
|
||||||
|
|
||||||
|
rk, err = x509.ParsePKCS1PrivateKey(pb.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid %s text %.32s... - %s", key_name, string(rsa_key_text), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return rk, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
func make_http_auth_config(cfg *HttpAuthConfig) (*hodu.HttpAuthConfig, error) {
|
func make_http_auth_config(cfg *HttpAuthConfig) (*hodu.HttpAuthConfig, error) {
|
||||||
var config hodu.HttpAuthConfig
|
var config hodu.HttpAuthConfig
|
||||||
var cred string
|
var cred string
|
||||||
var b []byte
|
var b []byte
|
||||||
var x []string
|
var x []string
|
||||||
var rsa_key_text []byte
|
|
||||||
var rk *rsa.PrivateKey
|
var rk *rsa.PrivateKey
|
||||||
var pb *pem.Block
|
|
||||||
var rule HttpAccessRule
|
var rule HttpAccessRule
|
||||||
var idx int
|
var idx int
|
||||||
var err error
|
var err error
|
||||||
@@ -428,27 +579,8 @@ func make_http_auth_config(cfg *HttpAuthConfig) (*hodu.HttpAuthConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load rsa key
|
// load rsa key
|
||||||
if cfg.TokenRsaKeyText == "" && cfg.TokenRsaKeyFile != "" {
|
rk, err = make_rsa_private_key_config(cfg.TokenRsaKeyText, cfg.TokenRsaKeyFile, hodu_rsa_key_text, "token rsa key")
|
||||||
rsa_key_text, err = os.ReadFile(cfg.TokenRsaKeyFile)
|
if err != nil { return nil, err }
|
||||||
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 {
|
|
||||||
// show up to first 8 characters only
|
|
||||||
return nil, fmt.Errorf("invalid token rsa key text %.32s... - no block or too many blocks", string(rsa_key_text))
|
|
||||||
}
|
|
||||||
|
|
||||||
rk, err = x509.ParsePKCS1PrivateKey(pb.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
// show up to first 8 characters only
|
|
||||||
return nil, fmt.Errorf("invalid token rsa key text %.32s... - %s", string(rsa_key_text), err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
config.TokenRsaKey = rk
|
config.TokenRsaKey = rk
|
||||||
|
|
||||||
// load access rules
|
// load access rules
|
||||||
@@ -469,6 +601,8 @@ func make_http_auth_config(cfg *HttpAuthConfig) (*hodu.HttpAuthConfig, error) {
|
|||||||
action = hodu.HTTP_ACCESS_REJECT
|
action = hodu.HTTP_ACCESS_REJECT
|
||||||
case "auth-required":
|
case "auth-required":
|
||||||
action = hodu.HTTP_ACCESS_AUTH_REQUIRED
|
action = hodu.HTTP_ACCESS_AUTH_REQUIRED
|
||||||
|
case "cert-required":
|
||||||
|
action = hodu.HTTP_ACCESS_CERT_REQUIRED
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid access rule action %s", rule.Action)
|
return nil, fmt.Errorf("invalid access rule action %s", rule.Action)
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-12
@@ -99,7 +99,7 @@ func (sh *signal_handler) WriteLog(id string, level hodu.LogLevel, fmt string, a
|
|||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy_addrs []string, wpx_addrs []string, cfg *ServerConfig) error {
|
func server_main(ctl_addrs []string, ect_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy_addrs []string, wpx_addrs []string, cfg *ServerConfig) error {
|
||||||
var s *hodu.Server
|
var s *hodu.Server
|
||||||
var config *hodu.ServerConfig
|
var config *hodu.ServerConfig
|
||||||
var logger *AppLogger
|
var logger *AppLogger
|
||||||
@@ -111,6 +111,7 @@ func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy
|
|||||||
|
|
||||||
config = &hodu.ServerConfig{
|
config = &hodu.ServerConfig{
|
||||||
CtlAddrs: ctl_addrs,
|
CtlAddrs: ctl_addrs,
|
||||||
|
EctAddrs: ect_addrs,
|
||||||
RpcAddrs: rpc_addrs,
|
RpcAddrs: rpc_addrs,
|
||||||
RpxAddrs: rpx_addrs,
|
RpxAddrs: rpx_addrs,
|
||||||
PxyAddrs: pxy_addrs,
|
PxyAddrs: pxy_addrs,
|
||||||
@@ -120,6 +121,8 @@ func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy
|
|||||||
// load configuration from cfg
|
// load configuration from cfg
|
||||||
config.CtlTls, err = make_tls_server_config(&cfg.CTL.TLS)
|
config.CtlTls, err = make_tls_server_config(&cfg.CTL.TLS)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
|
config.EctTls, err = make_tls_server_config(&cfg.ECT.TLS)
|
||||||
|
if err != nil { return err }
|
||||||
config.RpcTls, err = make_tls_server_config(&cfg.RPC.TLS)
|
config.RpcTls, err = make_tls_server_config(&cfg.RPC.TLS)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
config.RpxTls, err = make_tls_server_config(&cfg.RPX.TLS)
|
config.RpxTls, err = make_tls_server_config(&cfg.RPX.TLS)
|
||||||
@@ -128,27 +131,47 @@ func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy
|
|||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
config.PxyTargetTls, err = make_tls_client_config(&cfg.PXY.Target.TLS)
|
config.PxyTargetTls, err = make_tls_client_config(&cfg.PXY.Target.TLS)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
|
config.PxyAuth, err = make_http_auth_config(&cfg.PXY.Service.Auth)
|
||||||
|
if err != nil { return err }
|
||||||
config.WpxTls, err = make_tls_server_config(&cfg.WPX.TLS)
|
config.WpxTls, err = make_tls_server_config(&cfg.WPX.TLS)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
|
config.WpxAuth, err = make_http_auth_config(&cfg.WPX.Service.Auth)
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
if len(config.CtlAddrs) <= 0 { config.CtlAddrs = cfg.CTL.Service.Addrs }
|
if len(config.CtlAddrs) <= 0 { config.CtlAddrs = cfg.CTL.Service.Addrs }
|
||||||
|
if len(config.EctAddrs) <= 0 { config.EctAddrs = cfg.ECT.Service.Addrs }
|
||||||
if len(config.RpcAddrs) <= 0 { config.RpcAddrs = cfg.RPC.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.RpxAddrs) <= 0 { config.RpxAddrs = cfg.RPX.Service.Addrs }
|
||||||
if len(config.PxyAddrs) <= 0 { config.PxyAddrs = cfg.PXY.Service.Addrs }
|
if len(config.PxyAddrs) <= 0 { config.PxyAddrs = cfg.PXY.Service.Addrs }
|
||||||
if len(config.WpxAddrs) <= 0 { config.WpxAddrs = cfg.WPX.Service.Addrs }
|
if len(config.WpxAddrs) <= 0 { config.WpxAddrs = cfg.WPX.Service.Addrs }
|
||||||
|
|
||||||
|
config.RptyClientTokenProtection = cfg.CTL.Rpty.ClientToken.Protection
|
||||||
|
config.RptyClientTokenTtl, err = hodu.ParseDurationString(cfg.CTL.Rpty.ClientToken.TokenTtl)
|
||||||
|
if err != nil { return err }
|
||||||
|
config.RptyClientTokenRsaKey, err = make_rsa_private_key_config(cfg.CTL.Rpty.ClientToken.TokenRsaKeyText, cfg.CTL.Rpty.ClientToken.TokenRsaKeyFile, hodu_rsa_key_text, "rpty client token rsa key")
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
config.RpxClientTokenAttrName = cfg.RPX.ClientToken.AttrName
|
config.RpxClientTokenAttrName = cfg.RPX.ClientToken.AttrName
|
||||||
|
config.RpxClientTokenSubmatchIndex = cfg.RPX.ClientToken.SubmatchIndex
|
||||||
if cfg.RPX.ClientToken.Regex != "" {
|
if cfg.RPX.ClientToken.Regex != "" {
|
||||||
config.RpxClientTokenRegex, err = regexp.Compile(cfg.RPX.ClientToken.Regex)
|
config.RpxClientTokenRegex, err = regexp.Compile(cfg.RPX.ClientToken.Regex)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
}
|
}
|
||||||
config.RpxClientTokenSubmatchIndex = cfg.RPX.ClientToken.SubmatchIndex
|
|
||||||
|
config.RpxClientTokenProtection = cfg.RPX.ClientToken.Protection
|
||||||
|
config.RpxClientTokenTtl, err = hodu.ParseDurationString(cfg.RPX.ClientToken.TokenTtl)
|
||||||
|
if err != nil { return err }
|
||||||
|
config.RpxClientTokenRsaKey, err = make_rsa_private_key_config(cfg.RPX.ClientToken.TokenRsaKeyText, cfg.RPX.ClientToken.TokenRsaKeyFile, hodu_rsa_key_text, "rpx client token rsa key")
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
config.CtlCors = cfg.CTL.Service.Cors
|
config.CtlCors = cfg.CTL.Service.Cors
|
||||||
config.CtlAuth, err = make_http_auth_config(&cfg.CTL.Service.Auth)
|
config.CtlAuth, err = make_http_auth_config(&cfg.CTL.Service.Auth)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
|
|
||||||
config.CtlPrefix = cfg.CTL.Service.Prefix
|
config.CtlPrefix = cfg.CTL.Service.Prefix
|
||||||
|
|
||||||
|
config.EctAuth, err = make_http_auth_config(&cfg.ECT.Service.Auth)
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
config.RpcMaxConns = cfg.APP.MaxRpcConns
|
config.RpcMaxConns = cfg.APP.MaxRpcConns
|
||||||
config.RpcMinPingIntvl = cfg.APP.MinRpcPingIntvl
|
config.RpcMinPingIntvl = cfg.APP.MinRpcPingIntvl
|
||||||
config.MaxPeers = cfg.APP.MaxPeers
|
config.MaxPeers = cfg.APP.MaxPeers
|
||||||
@@ -205,6 +228,7 @@ func server_main(ctl_addrs []string, rpc_addrs []string, rpx_addrs[] string, pxy
|
|||||||
|
|
||||||
s.StartService(nil)
|
s.StartService(nil)
|
||||||
s.StartCtlService()
|
s.StartCtlService()
|
||||||
|
s.StartEctService()
|
||||||
s.StartRpxService()
|
s.StartRpxService()
|
||||||
s.StartPxyService()
|
s.StartPxyService()
|
||||||
s.StartWpxService()
|
s.StartWpxService()
|
||||||
@@ -341,14 +365,14 @@ func client_main(ctl_addrs []string, rpc_addrs []string, route_configs []string,
|
|||||||
config.HttpMaxHeaderBytes = cfg.APP.HttpMaxHeaderBytes
|
config.HttpMaxHeaderBytes = cfg.APP.HttpMaxHeaderBytes
|
||||||
|
|
||||||
if cfg.APP.TokenText != "" {
|
if cfg.APP.TokenText != "" {
|
||||||
config.Token = cfg.APP.TokenText
|
config.Token = strings.TrimSpace(cfg.APP.TokenText)
|
||||||
} else if cfg.APP.TokenFile != "" {
|
} else if cfg.APP.TokenFile != "" {
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
bytes, err = os.ReadFile(cfg.APP.TokenFile)
|
bytes, err = os.ReadFile(cfg.APP.TokenFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read token file - %s", err.Error())
|
return fmt.Errorf("unable to read token file - %s", err.Error())
|
||||||
}
|
}
|
||||||
config.Token = string(bytes)
|
config.Token = strings.TrimSpace(string(bytes))
|
||||||
}
|
}
|
||||||
// end of loading configuration from cfg
|
// end of loading configuration from cfg
|
||||||
|
|
||||||
@@ -408,6 +432,7 @@ func main() {
|
|||||||
if strings.EqualFold(os.Args[1], "server") {
|
if strings.EqualFold(os.Args[1], "server") {
|
||||||
var rpc_addrs []string
|
var rpc_addrs []string
|
||||||
var ctl_addrs []string
|
var ctl_addrs []string
|
||||||
|
var ect_addrs []string
|
||||||
var rpx_addrs []string
|
var rpx_addrs []string
|
||||||
var pxy_addrs []string
|
var pxy_addrs []string
|
||||||
var wpx_addrs []string
|
var wpx_addrs []string
|
||||||
@@ -418,6 +443,7 @@ func main() {
|
|||||||
var cfg ServerConfig
|
var cfg ServerConfig
|
||||||
|
|
||||||
ctl_addrs = make([]string, 0)
|
ctl_addrs = make([]string, 0)
|
||||||
|
ect_addrs = make([]string, 0)
|
||||||
rpc_addrs = make([]string, 0)
|
rpc_addrs = make([]string, 0)
|
||||||
rpx_addrs = make([]string, 0)
|
rpx_addrs = make([]string, 0)
|
||||||
pxy_addrs = make([]string, 0)
|
pxy_addrs = make([]string, 0)
|
||||||
@@ -428,6 +454,10 @@ func main() {
|
|||||||
ctl_addrs = append(ctl_addrs, v)
|
ctl_addrs = append(ctl_addrs, v)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
flgs.Func("ect-on", "specify a listening address for limited external control channel", func(v string) error {
|
||||||
|
ect_addrs = append(ect_addrs, v)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
flgs.Func("rpc-on", "specify a rpc listening address", func(v string) error {
|
flgs.Func("rpc-on", "specify a rpc listening address", func(v string) error {
|
||||||
rpc_addrs = append(rpc_addrs, v)
|
rpc_addrs = append(rpc_addrs, v)
|
||||||
return nil
|
return nil
|
||||||
@@ -502,7 +532,7 @@ func main() {
|
|||||||
cfg.APP.PtyShell = pty_shell
|
cfg.APP.PtyShell = pty_shell
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server_main(ctl_addrs, rpc_addrs, rpx_addrs, pxy_addrs, wpx_addrs, &cfg)
|
err = server_main(ctl_addrs, ect_addrs, rpc_addrs, rpx_addrs, pxy_addrs, wpx_addrs, &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: server error - %s\n", err.Error())
|
fmt.Fprintf(os.Stderr, "ERROR: server error - %s\n", err.Error())
|
||||||
goto oops
|
goto oops
|
||||||
@@ -548,10 +578,6 @@ func main() {
|
|||||||
pty_shell = v
|
pty_shell = v
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
flgs.Func("rxc-profile-files", "specify a file pattern for rxc profiles", func(v string) error {
|
|
||||||
rxc_profile_files = append(rxc_profile_files, v)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
flgs.Func("client-token", "specify a client token", func(v string) error {
|
flgs.Func("client-token", "specify a client token", func(v string) error {
|
||||||
client_token = v
|
client_token = v
|
||||||
return nil
|
return nil
|
||||||
@@ -560,6 +586,10 @@ func main() {
|
|||||||
rpx_target_addr = v
|
rpx_target_addr = v
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
flgs.Func("rxc-profile-files", "specify a file pattern for rxc profiles", func(v string) error {
|
||||||
|
rxc_profile_files = append(rxc_profile_files, v)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
flgs.SetOutput(io.Discard)
|
flgs.SetOutput(io.Discard)
|
||||||
err = flgs.Parse(os.Args[2:])
|
err = flgs.Parse(os.Args[2:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -625,8 +655,8 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
||||||
wrong_usage:
|
wrong_usage:
|
||||||
fmt.Fprintf(os.Stderr, "USAGE: %s server --rpc-on=addr:port --ctl-on=addr:port --rpx-on=addr:port --pxy-on=addr:port --wpx-on=addr:port [--config-file=file] [--config-file-pattern=pattern] [--pty-shell=string]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "USAGE: %s server --rpc-on=addr:port --ctl-on=addr:port --ect-on=addr:port --rpx-on=addr:port --pxy-on=addr:port --wpx-on=addr:port [--config-file=file] [--config-file-pattern=pattern] [--pty-shell=string]\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] [--pty-shell=string] [--rxc-profile-files=pattern] [--client-token=string] [--rpx-target-addr=addr:port] [peer-addr:peer-port ...]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s client --rpc-to=addr:port --ctl-on=addr:port [--config-file=file] [--config-file-pattern=pattern] [--pty-shell=string] [--rxc-profile-files=pattern] [--client-token=string] [--rpx-target-addr=addr:port] [local-target-addr:local-target-port,remote-binding-addr:remote-binding-port,type,description ...]\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " %s version\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s version\n", os.Args[0])
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import "strings"
|
|||||||
import "sync"
|
import "sync"
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
const HODU_RPC_VERSION uint32 = 0x010000
|
const HODU_RPC_VERSION uint32 = 0x010000
|
||||||
|
|
||||||
type LogLevel int
|
type LogLevel int
|
||||||
@@ -56,11 +58,19 @@ type Service interface {
|
|||||||
WriteLog(id string, level LogLevel, fmtstr string, args ...interface{})
|
WriteLog(id string, level LogLevel, fmtstr string, args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HttpHandlerExtraInfo struct {
|
||||||
|
// more information about the request processed
|
||||||
|
extra_id string
|
||||||
|
}
|
||||||
|
type http_request_context_key int
|
||||||
|
const http_handler_extra_info_key http_request_context_key = iota
|
||||||
|
|
||||||
type HttpAccessAction int
|
type HttpAccessAction int
|
||||||
const (
|
const (
|
||||||
HTTP_ACCESS_ACCEPT HttpAccessAction = iota
|
HTTP_ACCESS_ACCEPT HttpAccessAction = iota
|
||||||
HTTP_ACCESS_REJECT
|
HTTP_ACCESS_REJECT
|
||||||
HTTP_ACCESS_AUTH_REQUIRED
|
HTTP_ACCESS_AUTH_REQUIRED
|
||||||
|
HTTP_ACCESS_CERT_REQUIRED
|
||||||
)
|
)
|
||||||
|
|
||||||
type HttpAccessRule struct {
|
type HttpAccessRule struct {
|
||||||
@@ -125,6 +135,7 @@ type json_xterm_ws_event struct {
|
|||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
/*
|
||||||
//go:embed xterm.js
|
//go:embed xterm.js
|
||||||
var xterm_js []byte
|
var xterm_js []byte
|
||||||
//go:embed xterm-addon-fit.js
|
//go:embed xterm-addon-fit.js
|
||||||
@@ -133,6 +144,7 @@ var xterm_addon_fit_js []byte
|
|||||||
var xterm_addon_unicode11_js []byte
|
var xterm_addon_unicode11_js []byte
|
||||||
//go:embed xterm.css
|
//go:embed xterm.css
|
||||||
var xterm_css []byte
|
var xterm_css []byte
|
||||||
|
*/
|
||||||
//go:embed xterm.html
|
//go:embed xterm.html
|
||||||
var xterm_html string
|
var xterm_html string
|
||||||
|
|
||||||
@@ -369,7 +381,26 @@ func (stats *json_out_go_stats) from_runtime_stats() {
|
|||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
func (auth *HttpAuthConfig) Authenticate(req *http.Request) (int, string) {
|
func (auth *HttpAuthConfig) check_access_token(jwt_token string) error {
|
||||||
|
var jwt *JWT[ServerTokenClaim]
|
||||||
|
var claim ServerTokenClaim
|
||||||
|
var err error
|
||||||
|
|
||||||
|
jwt = NewJWT(auth.TokenRsaKey, &claim)
|
||||||
|
err = jwt.VerifyRS256(strings.TrimSpace(jwt_token))
|
||||||
|
if err == nil {
|
||||||
|
// verification ok. let's check the actual payload
|
||||||
|
var now time.Time
|
||||||
|
now = time.Now()
|
||||||
|
// TODO: subject check. other claim check
|
||||||
|
if !now.Before(time.Unix(claim.IssuedAt, 0)) && now.Before(time.Unix(claim.ExpiresAt, 0)) { return nil } // not expired
|
||||||
|
return fmt.Errorf("access token expired");
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to verify access token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *HttpAuthConfig) Authenticate(req *http.Request, access_token_param_name string) (int, string) {
|
||||||
var rule HttpAccessRule
|
var rule HttpAccessRule
|
||||||
var raddrport netip.AddrPort
|
var raddrport netip.AddrPort
|
||||||
var raddr netip.Addr
|
var raddr netip.Addr
|
||||||
@@ -404,6 +435,12 @@ func (auth *HttpAuthConfig) Authenticate(req *http.Request) (int, string) {
|
|||||||
return http.StatusOK, ""
|
return http.StatusOK, ""
|
||||||
} else if rule.Action == HTTP_ACCESS_REJECT {
|
} else if rule.Action == HTTP_ACCESS_REJECT {
|
||||||
return http.StatusForbidden, ""
|
return http.StatusForbidden, ""
|
||||||
|
} else if rule.Action == HTTP_ACCESS_CERT_REQUIRED {
|
||||||
|
if req.TLS == nil || len(req.TLS.PeerCertificates) <= 0 {
|
||||||
|
return http.StatusForbidden, ""
|
||||||
|
} else {
|
||||||
|
return http.StatusOK, ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP_ACCESS_AUTH_REQUIRED.
|
// HTTP_ACCESS_AUTH_REQUIRED.
|
||||||
@@ -423,19 +460,21 @@ func (auth *HttpAuthConfig) Authenticate(req *http.Request) (int, string) {
|
|||||||
auth_hdr = req.Header.Get("Authorization")
|
auth_hdr = req.Header.Get("Authorization")
|
||||||
if auth_hdr != "" {
|
if auth_hdr != "" {
|
||||||
var auth_parts []string
|
var auth_parts []string
|
||||||
|
|
||||||
auth_parts = strings.Fields(auth_hdr)
|
auth_parts = strings.Fields(auth_hdr)
|
||||||
if len(auth_parts) == 2 && strings.EqualFold(auth_parts[0], "Bearer") && auth.TokenRsaKey != nil {
|
if len(auth_parts) == 2 && strings.EqualFold(auth_parts[0], "Bearer") && auth.TokenRsaKey != nil {
|
||||||
var jwt *JWT[ServerTokenClaim]
|
err = auth.check_access_token(auth_parts[1])
|
||||||
var claim ServerTokenClaim
|
if err != nil { return http.StatusUnauthorized, "" } // not returning auth.Realm becuase it sents the Authorization header
|
||||||
jwt = NewJWT(auth.TokenRsaKey, &claim)
|
return http.StatusOK, ""
|
||||||
err = jwt.VerifyRS512(strings.TrimSpace(auth_parts[1]))
|
}
|
||||||
if err == nil {
|
} else if access_token_param_name != "" {
|
||||||
// verification ok. let's check the actual payload
|
// there is no Authorization header.
|
||||||
var now time.Time
|
// but there may be a token parameter.
|
||||||
now = time.Now()
|
var access_token string
|
||||||
if !now.Before(time.Unix(claim.IssuedAt, 0)) && now.Before(time.Unix(claim.ExpiresAt, 0)) { return http.StatusOK, "" } // not expired
|
access_token = req.FormValue(access_token_param_name)
|
||||||
}
|
if access_token != "" {
|
||||||
|
err = auth.check_access_token(access_token)
|
||||||
|
if err != nil { return http.StatusUnauthorized, "" } // not returning auth.Realm because it sent an access token
|
||||||
|
return http.StatusOK, ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +597,11 @@ func get_regex_submatch(re *regexp.Regexp, str string, n int) string {
|
|||||||
return str[start:end]
|
return str[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func append_raw_query(location string, req *http.Request) string {
|
||||||
|
if req.URL.RawQuery == "" { return location }
|
||||||
|
return location + "?" + req.URL.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
func read_line_limited(r *bufio.Reader, max int) (string, error) {
|
func read_line_limited(r *bufio.Reader, max int) (string, error) {
|
||||||
var b []byte
|
var b []byte
|
||||||
var line []byte
|
var line []byte
|
||||||
@@ -582,3 +626,27 @@ func read_line_limited(r *bufio.Reader, max int) (string, error) {
|
|||||||
return string(line), err
|
return string(line), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func read_fd_full(fd int, buf []byte) error {
|
||||||
|
var off int
|
||||||
|
|
||||||
|
off = 0
|
||||||
|
for off < len(buf) {
|
||||||
|
n, err := unix.Read(fd, buf[off:])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, unix.EINTR) { continue }
|
||||||
|
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) {
|
||||||
|
// for nonblocking fds, wait/retry as appropriate
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if off == len(buf) { return nil }
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -182,7 +182,7 @@ func TestHttpAuthConfigAuthenticateWithEncodedHeaders(t *testing.T) {
|
|||||||
req.Header.Set("X-Auth-Username", username)
|
req.Header.Set("X-Auth-Username", username)
|
||||||
req.Header.Set("X-Auth-Password", password)
|
req.Header.Set("X-Auth-Password", password)
|
||||||
|
|
||||||
status, realm = auth.Authenticate(req)
|
status, realm = auth.Authenticate(req, "")
|
||||||
if status != http.StatusOK || realm != "" {
|
if status != http.StatusOK || realm != "" {
|
||||||
t.Fatalf("unexpected auth result status=%d realm=%q", status, realm)
|
t.Fatalf("unexpected auth result status=%d realm=%q", status, realm)
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ func TestHttpAuthConfigAuthenticateRejectsInvalidBase64(t *testing.T) {
|
|||||||
req.RemoteAddr = "127.0.0.1:12345"
|
req.RemoteAddr = "127.0.0.1:12345"
|
||||||
req.Header.Set("X-Auth-Username", "%%%")
|
req.Header.Set("X-Auth-Username", "%%%")
|
||||||
|
|
||||||
status, _ = auth.Authenticate(req)
|
status, _ = auth.Authenticate(req, "")
|
||||||
if status != http.StatusBadRequest {
|
if status != http.StatusBadRequest {
|
||||||
t.Fatalf("expected bad request for invalid header encoding, got %d", status)
|
t.Fatalf("expected bad request for invalid header encoding, got %d", status)
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ func TestHttpAuthConfigAccessRuleReject(t *testing.T) {
|
|||||||
req = httptest.NewRequest(http.MethodGet, "http://example.com/blocked/path", nil)
|
req = httptest.NewRequest(http.MethodGet, "http://example.com/blocked/path", nil)
|
||||||
req.RemoteAddr = "127.0.0.1:12345"
|
req.RemoteAddr = "127.0.0.1:12345"
|
||||||
|
|
||||||
status, _ = auth.Authenticate(req)
|
status, _ = auth.Authenticate(req, "")
|
||||||
if status != http.StatusForbidden {
|
if status != http.StatusForbidden {
|
||||||
t.Fatalf("expected forbidden status, got %d", status)
|
t.Fatalf("expected forbidden status, got %d", status)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
b64() { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 generate private-key-file [ttl-seconds]" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="$1"
|
||||||
|
key_file="$2"
|
||||||
|
ttl="$3"
|
||||||
|
|
||||||
|
if [ "$cmd" != "generate" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$key_file" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ttl" ]; then
|
||||||
|
ttl=3600
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ttl" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
echo "invalid ttl-seconds: $ttl" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
now=$(date +%s)
|
||||||
|
exp=$((now + ttl))
|
||||||
|
|
||||||
|
H=$(printf '{"alg":"RS256","typ":"JWT"}' | b64)
|
||||||
|
P=$(printf '{"sub":"123","iat":%s,"exp":%s}' "$now" "$exp" | b64)
|
||||||
|
D="$H.$P"
|
||||||
|
S=$(printf '%s' "$D" | openssl dgst -sha256 -sign "$key_file" | b64)
|
||||||
|
|
||||||
|
echo "$H.$P.$S"
|
||||||
@@ -14,35 +14,35 @@ import "strings"
|
|||||||
func Sign(data []byte, privkey *rsa.PrivateKey) ([]byte, error) {
|
func Sign(data []byte, privkey *rsa.PrivateKey) ([]byte, error) {
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
h = crypto.SHA512.New()
|
h = crypto.SHA256.New()
|
||||||
h.Write(data)
|
h.Write(data)
|
||||||
|
|
||||||
//fmt.Printf("%+v\n", h.Sum(nil))
|
//fmt.Printf("%+v\n", h.Sum(nil))
|
||||||
return rsa.SignPKCS1v15(rand.Reader, privkey, crypto.SHA512, h.Sum(nil))
|
return rsa.SignPKCS1v15(rand.Reader, privkey, crypto.SHA256, h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Verify(data []byte, pubkey *rsa.PublicKey, sig []byte) error {
|
func Verify(data []byte, pubkey *rsa.PublicKey, sig []byte) error {
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
h = crypto.SHA512.New()
|
h = crypto.SHA256.New()
|
||||||
h.Write(data)
|
h.Write(data)
|
||||||
|
|
||||||
return rsa.VerifyPKCS1v15(pubkey, crypto.SHA512, h.Sum(nil), sig)
|
return rsa.VerifyPKCS1v15(pubkey, crypto.SHA256, h.Sum(nil), sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SignHS512(data []byte, key string) ([]byte, error) {
|
func SignHS256(data []byte, key string) ([]byte, error) {
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
h = hmac.New(crypto.SHA512.New, []byte(key))
|
h = hmac.New(crypto.SHA256.New, []byte(key))
|
||||||
h.Write(data)
|
h.Write(data)
|
||||||
|
|
||||||
return h.Sum(nil), nil
|
return h.Sum(nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyHS512(data []byte, key string, sig []byte) error {
|
func VerifyHS256(data []byte, key string, sig []byte) error {
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
h = crypto.SHA512.New()
|
h = crypto.SHA256.New()
|
||||||
h.Write(data)
|
h.Write(data)
|
||||||
|
|
||||||
if !hmac.Equal(h.Sum(nil), sig) { return fmt.Errorf("invalid signature") }
|
if !hmac.Equal(h.Sum(nil), sig) { return fmt.Errorf("invalid signature") }
|
||||||
@@ -66,7 +66,7 @@ func NewJWT[T any](key *rsa.PrivateKey, claims *T) *JWT[T] {
|
|||||||
return &JWT[T]{key: key, claims: claims}
|
return &JWT[T]{key: key, claims: claims}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JWT[T]) SignRS512() (string, error) {
|
func (j *JWT[T]) SignRS256() (string, error) {
|
||||||
var h JWTHeader
|
var h JWTHeader
|
||||||
var hb []byte
|
var hb []byte
|
||||||
var cb []byte
|
var cb []byte
|
||||||
@@ -75,7 +75,7 @@ func (j *JWT[T]) SignRS512() (string, error) {
|
|||||||
var hs hash.Hash
|
var hs hash.Hash
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
h.Algo = "RS512"
|
h.Algo = "RS256"
|
||||||
h.Type = "JWT"
|
h.Type = "JWT"
|
||||||
|
|
||||||
hb, err = json.Marshal(h)
|
hb, err = json.Marshal(h)
|
||||||
@@ -86,17 +86,17 @@ func (j *JWT[T]) SignRS512() (string, error) {
|
|||||||
|
|
||||||
ss = base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(cb)
|
ss = base64.RawURLEncoding.EncodeToString(hb) + "." + base64.RawURLEncoding.EncodeToString(cb)
|
||||||
|
|
||||||
hs = crypto.SHA512.New()
|
hs = crypto.SHA256.New()
|
||||||
hs.Write([]byte(ss))
|
hs.Write([]byte(ss))
|
||||||
|
|
||||||
sb, err = rsa.SignPKCS1v15(rand.Reader, j.key, crypto.SHA512, hs.Sum(nil))
|
sb, err = rsa.SignPKCS1v15(rand.Reader, j.key, crypto.SHA256, hs.Sum(nil))
|
||||||
if err != nil { return "", err }
|
if err != nil { return "", err }
|
||||||
|
|
||||||
//fmt.Printf ("%+v %+v %s\n", string(hb), string(cb), (ss + "." + base64.RawURLEncoding.EncodeToString(sb)))
|
//fmt.Printf ("%+v %+v %s\n", string(hb), string(cb), (ss + "." + base64.RawURLEncoding.EncodeToString(sb)))
|
||||||
return ss + "." + base64.RawURLEncoding.EncodeToString(sb), nil
|
return ss + "." + base64.RawURLEncoding.EncodeToString(sb), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JWT[T]) VerifyRS512(tok string) error {
|
func (j *JWT[T]) VerifyRS256(tok string) error {
|
||||||
var segs []string
|
var segs []string
|
||||||
var hb []byte
|
var hb []byte
|
||||||
var cb []byte
|
var cb []byte
|
||||||
@@ -113,7 +113,7 @@ func (j *JWT[T]) VerifyRS512(tok string) error {
|
|||||||
err = json.Unmarshal(hb, &jh)
|
err = json.Unmarshal(hb, &jh)
|
||||||
if err != nil { return fmt.Errorf("invalid header - %s", err.Error()) }
|
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) }
|
if jh.Algo != "RS256" || jh.Type != "JWT" { return fmt.Errorf("invalid header content %+v", jh) }
|
||||||
|
|
||||||
cb, err = base64.RawURLEncoding.DecodeString(segs[1])
|
cb, err = base64.RawURLEncoding.DecodeString(segs[1])
|
||||||
if err != nil { return fmt.Errorf("invalid claims - %s", err.Error()) }
|
if err != nil { return fmt.Errorf("invalid claims - %s", err.Error()) }
|
||||||
@@ -123,11 +123,11 @@ func (j *JWT[T]) VerifyRS512(tok string) error {
|
|||||||
ss, err = base64.RawURLEncoding.DecodeString(segs[2])
|
ss, err = base64.RawURLEncoding.DecodeString(segs[2])
|
||||||
if err != nil { return fmt.Errorf("invalid signature - %s", err.Error()) }
|
if err != nil { return fmt.Errorf("invalid signature - %s", err.Error()) }
|
||||||
|
|
||||||
hs = crypto.SHA512.New()
|
hs = crypto.SHA256.New()
|
||||||
hs.Write([]byte(segs[0]))
|
hs.Write([]byte(segs[0]))
|
||||||
hs.Write([]byte("."))
|
hs.Write([]byte("."))
|
||||||
hs.Write([]byte(segs[1]))
|
hs.Write([]byte(segs[1]))
|
||||||
err = rsa.VerifyPKCS1v15(&j.key.PublicKey, crypto.SHA512, hs.Sum(nil), ss)
|
err = rsa.VerifyPKCS1v15(&j.key.PublicKey, crypto.SHA256, hs.Sum(nil), ss)
|
||||||
if err != nil { return fmt.Errorf("unverifiable signature - %s", err.Error()) }
|
if err != nil { return fmt.Errorf("unverifiable signature - %s", err.Error()) }
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+2
-2
@@ -28,13 +28,13 @@ func TestJwt(t *testing.T) {
|
|||||||
|
|
||||||
var j *hodu.JWT[jwt_claim]
|
var j *hodu.JWT[jwt_claim]
|
||||||
j = hodu.NewJWT(key, &jc)
|
j = hodu.NewJWT(key, &jc)
|
||||||
tok, err = j.SignRS512()
|
tok, err = j.SignRS256()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("signing failure - %s", err.Error())
|
t.Fatalf("signing failure - %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
jc = jwt_claim{}
|
jc = jwt_claim{}
|
||||||
err = j.VerifyRS512(tok)
|
err = j.VerifyRS256(tok)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("verification failure - %s", err.Error())
|
t.Fatalf("verification failure - %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "hodu-xterm-bundle",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:xterm": "vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
|
"@xterm/xterm": "^5.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-singlefile": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function fail(string $msg): void {
|
||||||
|
fwrite(STDERR, $msg . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage(): void {
|
||||||
|
fail(
|
||||||
|
"USAGE: rsa-aes-256-gcm.php encipher public-key-file text-to-encipher [ttl-seconds]\n" .
|
||||||
|
" rsa-aes-256-gcm.php decipher private-key-file [document]\n\n" .
|
||||||
|
"If document is omitted, stdin is used. ttl-seconds defaults to 30."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_key_text(string $path): string {
|
||||||
|
$text = @file_get_contents($path);
|
||||||
|
if ($text === false) {
|
||||||
|
fail("unable to read {$path}");
|
||||||
|
}
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_private_key(string $path) {
|
||||||
|
$key = openssl_pkey_get_private(load_key_text($path));
|
||||||
|
if ($key === false) {
|
||||||
|
fail("unable to load private key from {$path}");
|
||||||
|
}
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_public_key(string $path) {
|
||||||
|
$text = load_key_text($path);
|
||||||
|
$key = openssl_pkey_get_public($text);
|
||||||
|
if ($key !== false) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
$private_key = openssl_pkey_get_private($text);
|
||||||
|
if ($private_key === false) {
|
||||||
|
fail("unable to load public key from {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = openssl_pkey_get_details($private_key);
|
||||||
|
if ($details === false || !isset($details["key"])) {
|
||||||
|
fail("unable to extract public key from {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = openssl_pkey_get_public($details["key"]);
|
||||||
|
if ($key === false) {
|
||||||
|
fail("unable to extract public key from {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mgf1_sha256(string $seed, int $length): string {
|
||||||
|
$output = "";
|
||||||
|
$counter = 0;
|
||||||
|
|
||||||
|
while (strlen($output) < $length) {
|
||||||
|
$c = pack("N", $counter);
|
||||||
|
$output .= hash("sha256", $seed . $c, true);
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($output, 0, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function oaep_encode_sha256(string $msg, int $k): string {
|
||||||
|
$label_hash = hash("sha256", "", true);
|
||||||
|
$h_len = strlen($label_hash);
|
||||||
|
$m_len = strlen($msg);
|
||||||
|
$ps_len = $k - $m_len - (2 * $h_len) - 2;
|
||||||
|
|
||||||
|
if ($ps_len < 0) {
|
||||||
|
fail("message too long for rsa key");
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = $label_hash . str_repeat("\x00", $ps_len) . "\x01" . $msg;
|
||||||
|
$seed = random_bytes($h_len);
|
||||||
|
$db_mask = mgf1_sha256($seed, $k - $h_len - 1);
|
||||||
|
$masked_db = $db ^ $db_mask;
|
||||||
|
$seed_mask = mgf1_sha256($masked_db, $h_len);
|
||||||
|
$masked_seed = $seed ^ $seed_mask;
|
||||||
|
|
||||||
|
return "\x00" . $masked_seed . $masked_db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function oaep_decode_sha256(string $em, int $k): string {
|
||||||
|
$label_hash = hash("sha256", "", true);
|
||||||
|
$h_len = strlen($label_hash);
|
||||||
|
$em_len = strlen($em);
|
||||||
|
$one_pos = -1;
|
||||||
|
|
||||||
|
if ($em_len !== $k || $k < (2 * $h_len) + 2) {
|
||||||
|
fail("invalid oaep block size");
|
||||||
|
}
|
||||||
|
if ($em[0] !== "\x00") {
|
||||||
|
fail("invalid oaep prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
$masked_seed = substr($em, 1, $h_len);
|
||||||
|
$masked_db = substr($em, 1 + $h_len);
|
||||||
|
$seed_mask = mgf1_sha256($masked_db, $h_len);
|
||||||
|
$seed = $masked_seed ^ $seed_mask;
|
||||||
|
$db_mask = mgf1_sha256($seed, $k - $h_len - 1);
|
||||||
|
$db = $masked_db ^ $db_mask;
|
||||||
|
|
||||||
|
if (substr($db, 0, $h_len) !== $label_hash) {
|
||||||
|
fail("invalid oaep label hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = $h_len; $i < strlen($db); $i++) {
|
||||||
|
if ($db[$i] === "\x01") {
|
||||||
|
$one_pos = $i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($db[$i] !== "\x00") {
|
||||||
|
fail("invalid oaep padding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($one_pos < 0) {
|
||||||
|
fail("invalid oaep separator");
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($db, $one_pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rsa_oaep_sha256_encrypt($public_key, string $plaintext): string {
|
||||||
|
$details = openssl_pkey_get_details($public_key);
|
||||||
|
$ciphertext = "";
|
||||||
|
$ok = false;
|
||||||
|
|
||||||
|
if ($details === false || !isset($details["bits"])) {
|
||||||
|
fail("unable to get rsa public key details");
|
||||||
|
}
|
||||||
|
|
||||||
|
$encoded = oaep_encode_sha256($plaintext, intdiv((int)$details["bits"] + 7, 8));
|
||||||
|
$ok = openssl_public_encrypt($encoded, $ciphertext, $public_key, OPENSSL_NO_PADDING);
|
||||||
|
if (!$ok) {
|
||||||
|
fail("rsa encryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ciphertext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rsa_oaep_sha256_decrypt($private_key, string $ciphertext): string {
|
||||||
|
$details = openssl_pkey_get_details($private_key);
|
||||||
|
$encoded = "";
|
||||||
|
$ok = false;
|
||||||
|
|
||||||
|
if ($details === false || !isset($details["bits"])) {
|
||||||
|
fail("unable to get rsa private key details");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = openssl_private_decrypt($ciphertext, $encoded, $private_key, OPENSSL_NO_PADDING);
|
||||||
|
if (!$ok) {
|
||||||
|
fail("rsa decryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return oaep_decode_sha256($encoded, intdiv((int)$details["bits"] + 7, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode_url_base64(string $data): string {
|
||||||
|
return rtrim(strtr(base64_encode($data), "+/", "-_"), "=");
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode_url_base64(string $data) {
|
||||||
|
$padding_len = 0;
|
||||||
|
$decoded = "";
|
||||||
|
|
||||||
|
$padding_len = (4 - (strlen($data) % 4)) % 4;
|
||||||
|
$decoded = base64_decode(strtr($data . str_repeat("=", $padding_len), "-_", "+/"), true);
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encipher($public_key, string $text): string {
|
||||||
|
$aes_key = random_bytes(32);
|
||||||
|
$nonce = random_bytes(12);
|
||||||
|
$tag = "";
|
||||||
|
$ciphertext = openssl_encrypt($text, "aes-256-gcm", $aes_key, OPENSSL_RAW_DATA, $nonce, $tag, "", 16);
|
||||||
|
|
||||||
|
if ($ciphertext === false) {
|
||||||
|
fail("aes-gcm encryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
$encrypted_key = rsa_oaep_sha256_encrypt($public_key, $aes_key);
|
||||||
|
$ciphertext .= $tag;
|
||||||
|
|
||||||
|
return encode_url_base64($encrypted_key) . "." . encode_url_base64($nonce) . "." . encode_url_base64($ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decipher($private_key, string $doc): string {
|
||||||
|
$parts = explode(".", $doc, 3);
|
||||||
|
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
fail("invalid serialized token document");
|
||||||
|
}
|
||||||
|
|
||||||
|
$encrypted_key = decode_url_base64($parts[0]);
|
||||||
|
$nonce = decode_url_base64($parts[1]);
|
||||||
|
$ciphertext_and_tag = decode_url_base64($parts[2]);
|
||||||
|
|
||||||
|
if ($encrypted_key === false) {
|
||||||
|
fail("invalid encrypted key");
|
||||||
|
}
|
||||||
|
if ($nonce === false) {
|
||||||
|
fail("invalid nonce");
|
||||||
|
}
|
||||||
|
if ($ciphertext_and_tag === false) {
|
||||||
|
fail("invalid ciphertext");
|
||||||
|
}
|
||||||
|
if (strlen($nonce) !== 12) {
|
||||||
|
fail("invalid nonce size");
|
||||||
|
}
|
||||||
|
if (strlen($ciphertext_and_tag) < 16) {
|
||||||
|
fail("invalid ciphertext size");
|
||||||
|
}
|
||||||
|
|
||||||
|
$aes_key = rsa_oaep_sha256_decrypt($private_key, $encrypted_key);
|
||||||
|
$tag = substr($ciphertext_and_tag, -16);
|
||||||
|
$ciphertext = substr($ciphertext_and_tag, 0, -16);
|
||||||
|
$plaintext = openssl_decrypt($ciphertext, "aes-256-gcm", $aes_key, OPENSSL_RAW_DATA, $nonce, $tag, "");
|
||||||
|
|
||||||
|
if ($plaintext === false) {
|
||||||
|
fail("aes-gcm decryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function make_token_payload(string $token, int $now, int $ttl): string {
|
||||||
|
return rtrim(strtr(base64_encode($token), "+/", "-_"), "=") . "|" . (string)$now . "|" . (string)($now + $ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_token_payload(string $payload): string {
|
||||||
|
$parts = explode("|", $payload, 3);
|
||||||
|
$token = "";
|
||||||
|
$padding_len = 0;
|
||||||
|
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
fail("invalid protected token payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
$padding_len = (4 - (strlen($parts[0]) % 4)) % 4;
|
||||||
|
$token = base64_decode(strtr($parts[0] . str_repeat("=", $padding_len), "-_", "+/"), true);
|
||||||
|
if ($token === false) {
|
||||||
|
fail("invalid protected token text");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $token . "|" . $parts[1] . "|" . $parts[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($argc < 3) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $argv[1];
|
||||||
|
$key_file = $argv[2];
|
||||||
|
|
||||||
|
if ($mode === "encipher") {
|
||||||
|
$input = "";
|
||||||
|
$ttl = 30;
|
||||||
|
$now = time();
|
||||||
|
$public_key = load_public_key($key_file);
|
||||||
|
|
||||||
|
if ($argc >= 4) {
|
||||||
|
$input = $argv[3];
|
||||||
|
} else {
|
||||||
|
fail("missing token");
|
||||||
|
}
|
||||||
|
if ($argc >= 5) {
|
||||||
|
if (!ctype_digit($argv[4])) {
|
||||||
|
fail("invalid ttl-seconds");
|
||||||
|
}
|
||||||
|
$ttl = (int)$argv[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo encipher($public_key, make_token_payload($input, $now, $ttl)) . PHP_EOL;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode === "decipher") {
|
||||||
|
$input = $argc >= 4 ? $argv[3] : stream_get_contents(STDIN);
|
||||||
|
$private_key = load_private_key($key_file);
|
||||||
|
if ($input === false) {
|
||||||
|
fail("failed to read input");
|
||||||
|
}
|
||||||
|
echo parse_token_payload(decipher($private_key, $input)) . PHP_EOL;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
usage();
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||||
|
|
||||||
|
|
||||||
|
def fail(msg: str) -> None:
|
||||||
|
sys.stderr.write(msg + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def usage() -> None:
|
||||||
|
fail(
|
||||||
|
"USAGE: rsa-aes-256-gcm.py encipher public-key-file text-to-encipher [ttl-seconds]\n"
|
||||||
|
" rsa-aes-256-gcm.py decipher private-key-file [document]\n\n"
|
||||||
|
"If document is omitted, stdin is used. ttl-seconds defaults to 30."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_key_text(path: str) -> bytes:
|
||||||
|
try:
|
||||||
|
return open(path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
fail(f"unable to read {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_private_key(path: str):
|
||||||
|
try:
|
||||||
|
return load_pem_private_key(load_key_text(path), password=None)
|
||||||
|
except ValueError:
|
||||||
|
fail(f"unable to load private key from {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_public_key(path: str):
|
||||||
|
key_text = load_key_text(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return load_pem_public_key(key_text)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
private_key = load_pem_private_key(key_text, password=None)
|
||||||
|
except ValueError:
|
||||||
|
fail(f"unable to load public key from {path}")
|
||||||
|
|
||||||
|
return private_key.public_key()
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_oaep_sha256_encrypt(public_key, plaintext: bytes) -> bytes:
|
||||||
|
if not isinstance(public_key, rsa.RSAPublicKey):
|
||||||
|
fail("unable to get rsa public key details")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return public_key.encrypt(
|
||||||
|
plaintext,
|
||||||
|
padding.OAEP(
|
||||||
|
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
label=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
fail("rsa encryption failed")
|
||||||
|
|
||||||
|
|
||||||
|
def rsa_oaep_sha256_decrypt(private_key, ciphertext: bytes) -> bytes:
|
||||||
|
if not isinstance(private_key, rsa.RSAPrivateKey):
|
||||||
|
fail("unable to get rsa private key details")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return private_key.decrypt(
|
||||||
|
ciphertext,
|
||||||
|
padding.OAEP(
|
||||||
|
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
label=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
fail("rsa decryption failed")
|
||||||
|
|
||||||
|
|
||||||
|
def encode_url_base64(data: bytes) -> str:
|
||||||
|
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_url_base64(text: str) -> bytes:
|
||||||
|
padding_len = (-len(text)) % 4
|
||||||
|
|
||||||
|
try:
|
||||||
|
return base64.b64decode(text + ("=" * padding_len), altchars=b"-_", validate=True)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
|
||||||
|
def encipher(public_key, text: str) -> str:
|
||||||
|
aes_key = os.urandom(32)
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
ciphertext = AESGCM(aes_key).encrypt(nonce, text.encode("utf-8"), None)
|
||||||
|
encrypted_key = rsa_oaep_sha256_encrypt(public_key, aes_key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
encode_url_base64(encrypted_key)
|
||||||
|
+ "."
|
||||||
|
+ encode_url_base64(nonce)
|
||||||
|
+ "."
|
||||||
|
+ encode_url_base64(ciphertext)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decipher(private_key, doc: str) -> str:
|
||||||
|
parts = doc.split(".", 2)
|
||||||
|
|
||||||
|
if len(parts) != 3:
|
||||||
|
fail("invalid serialized token document")
|
||||||
|
|
||||||
|
try:
|
||||||
|
encrypted_key = decode_url_base64(parts[0])
|
||||||
|
except Exception:
|
||||||
|
fail("invalid encrypted key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
nonce = decode_url_base64(parts[1])
|
||||||
|
except Exception:
|
||||||
|
fail("invalid nonce")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ciphertext = decode_url_base64(parts[2])
|
||||||
|
except Exception:
|
||||||
|
fail("invalid ciphertext")
|
||||||
|
|
||||||
|
if len(nonce) != 12:
|
||||||
|
fail(f"invalid nonce size")
|
||||||
|
if len(ciphertext) < 16:
|
||||||
|
fail("invalid ciphertext size")
|
||||||
|
|
||||||
|
aes_key = rsa_oaep_sha256_decrypt(private_key, encrypted_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
plaintext = AESGCM(aes_key).decrypt(nonce, ciphertext, None)
|
||||||
|
except Exception:
|
||||||
|
fail("aes-gcm decryption failed")
|
||||||
|
|
||||||
|
return plaintext.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def make_token_payload(token: str, now: int, ttl: int) -> str:
|
||||||
|
return (
|
||||||
|
base64.urlsafe_b64encode(token.encode("utf-8")).decode("ascii").rstrip("=")
|
||||||
|
+ "|"
|
||||||
|
+ str(now)
|
||||||
|
+ "|"
|
||||||
|
+ str(now + ttl)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_token_payload(payload: str) -> str:
|
||||||
|
parts = payload.split("|", 2)
|
||||||
|
padding_len = 0
|
||||||
|
|
||||||
|
if len(parts) != 3:
|
||||||
|
fail("invalid protected token payload")
|
||||||
|
|
||||||
|
padding_len = (-len(parts[0])) % 4
|
||||||
|
try:
|
||||||
|
token = base64.urlsafe_b64decode(parts[0] + ("=" * padding_len)).decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
fail("invalid protected token text")
|
||||||
|
|
||||||
|
return token + "|" + parts[1] + "|" + parts[2]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
usage()
|
||||||
|
|
||||||
|
mode = sys.argv[1]
|
||||||
|
key_file = sys.argv[2]
|
||||||
|
|
||||||
|
if mode == "encipher":
|
||||||
|
now = 0
|
||||||
|
ttl = 30
|
||||||
|
input_text = ""
|
||||||
|
|
||||||
|
if len(sys.argv) >= 4:
|
||||||
|
input_text = sys.argv[3]
|
||||||
|
else:
|
||||||
|
fail("missing token")
|
||||||
|
if len(sys.argv) >= 5:
|
||||||
|
try:
|
||||||
|
ttl = int(sys.argv[4])
|
||||||
|
except ValueError:
|
||||||
|
fail("invalid ttl-seconds")
|
||||||
|
now = int(time.time())
|
||||||
|
public_key = load_public_key(key_file)
|
||||||
|
print(encipher(public_key, make_token_payload(input_text, now, ttl)))
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == "decipher":
|
||||||
|
input_text = ""
|
||||||
|
if len(sys.argv) >= 4:
|
||||||
|
input_text = sys.argv[3]
|
||||||
|
else:
|
||||||
|
input_text = sys.stdin.read()
|
||||||
|
private_key = load_private_key(key_file)
|
||||||
|
print(parse_token_payload(decipher(private_key, input_text)))
|
||||||
|
return
|
||||||
|
|
||||||
|
usage()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
package hodu
|
||||||
|
|
||||||
|
import "crypto/aes"
|
||||||
|
import "crypto/cipher"
|
||||||
|
import "crypto/rand"
|
||||||
|
import "crypto/rsa"
|
||||||
|
import "crypto/sha256"
|
||||||
|
import "encoding/base64"
|
||||||
|
import "fmt"
|
||||||
|
import "strconv"
|
||||||
|
import "strings"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// currently, it supports rsa-aes-128-gcm only.
|
||||||
|
type RSAAES struct {
|
||||||
|
key *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type RSAAESToken struct {
|
||||||
|
Token string
|
||||||
|
IssuedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode_url_base64(data []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_url_base64(text string) ([]byte, error) {
|
||||||
|
return base64.RawURLEncoding.DecodeString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRSAAES(key *rsa.PrivateKey) *RSAAES {
|
||||||
|
return &RSAAES{key: key}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RSAAES) Encipher(data []byte) (string, error) {
|
||||||
|
var aes_key []byte
|
||||||
|
var block cipher.Block
|
||||||
|
var gcm cipher.AEAD
|
||||||
|
var nonce []byte
|
||||||
|
var ciphertext []byte
|
||||||
|
var encrypted_key []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e.key == nil {
|
||||||
|
return "", fmt.Errorf("missing rsa key")
|
||||||
|
}
|
||||||
|
|
||||||
|
aes_key = make([]byte, 32)
|
||||||
|
_, err = rand.Read(aes_key)
|
||||||
|
if err != nil { return "", err }
|
||||||
|
|
||||||
|
block, err = aes.NewCipher(aes_key)
|
||||||
|
if err != nil { return "", err }
|
||||||
|
|
||||||
|
gcm, err = cipher.NewGCM(block)
|
||||||
|
if err != nil { return "", err }
|
||||||
|
|
||||||
|
nonce = make([]byte, gcm.NonceSize())
|
||||||
|
_, err = rand.Read(nonce)
|
||||||
|
if err != nil { return "", err }
|
||||||
|
|
||||||
|
ciphertext = gcm.Seal(nil, nonce, data, nil)
|
||||||
|
|
||||||
|
encrypted_key, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, &e.key.PublicKey, aes_key, nil)
|
||||||
|
if err != nil { return "", err }
|
||||||
|
|
||||||
|
return encode_url_base64(encrypted_key) +
|
||||||
|
"." + encode_url_base64(nonce) +
|
||||||
|
"." + encode_url_base64(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RSAAES) Decipher(doc string) ([]byte, error) {
|
||||||
|
var parts []string
|
||||||
|
var encrypted_key []byte
|
||||||
|
var nonce []byte
|
||||||
|
var ciphertext []byte
|
||||||
|
var aes_key []byte
|
||||||
|
var block cipher.Block
|
||||||
|
var gcm cipher.AEAD
|
||||||
|
var plaintext []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e.key == nil {
|
||||||
|
return nil, fmt.Errorf("missing rsa key")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = strings.Split(doc, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid serialized token document")
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted_key, err = decode_url_base64(parts[0])
|
||||||
|
if err != nil { return nil, fmt.Errorf("invalid encrypted key - %s", err.Error()) }
|
||||||
|
|
||||||
|
nonce, err = decode_url_base64(parts[1])
|
||||||
|
if err != nil { return nil, fmt.Errorf("invalid nonce - %s", err.Error()) }
|
||||||
|
|
||||||
|
ciphertext, err = decode_url_base64(parts[2])
|
||||||
|
if err != nil { return nil, fmt.Errorf("invalid ciphertext - %s", err.Error()) }
|
||||||
|
|
||||||
|
aes_key, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, e.key, encrypted_key, nil)
|
||||||
|
if err != nil { return nil, fmt.Errorf("failed to decrypt aes key - %s", err.Error()) }
|
||||||
|
|
||||||
|
block, err = aes.NewCipher(aes_key)
|
||||||
|
if err != nil { return nil, fmt.Errorf("invalid aes key - %s", err.Error()) }
|
||||||
|
|
||||||
|
gcm, err = cipher.NewGCM(block)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
if len(nonce) != gcm.NonceSize() {
|
||||||
|
return nil, fmt.Errorf("invalid nonce size %d", len(nonce))
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err = gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil { return nil, fmt.Errorf("failed to decrypt ciphertext - %s", err.Error()) }
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RSAAES) EncipherToken(token string, issued_at time.Time, expires_at time.Time) (string, error) {
|
||||||
|
var plain string
|
||||||
|
|
||||||
|
plain = base64.RawURLEncoding.EncodeToString([]byte(token)) +
|
||||||
|
"|" + strconv.FormatInt(issued_at.Unix(), 10) +
|
||||||
|
"|" + strconv.FormatInt(expires_at.Unix(), 10)
|
||||||
|
|
||||||
|
return e.Encipher([]byte(plain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RSAAES) DecipherToken(doc string, now time.Time) (*RSAAESToken, error) {
|
||||||
|
var data []byte
|
||||||
|
var parts []string
|
||||||
|
var token_data []byte
|
||||||
|
var issued_at_n int64
|
||||||
|
var expires_at_n int64
|
||||||
|
var token RSAAESToken
|
||||||
|
var err error
|
||||||
|
|
||||||
|
data, err = e.Decipher(doc)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
parts = strings.SplitN(string(data), "|", 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid protected token payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
token_data, err = base64.RawURLEncoding.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid protected token text - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
issued_at_n, err = strconv.ParseInt(parts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid protected token issued-at - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
expires_at_n, err = strconv.ParseInt(parts[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid protected token expiry - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token.Token = string(token_data)
|
||||||
|
token.IssuedAt = time.Unix(issued_at_n, 0)
|
||||||
|
token.ExpiresAt = time.Unix(expires_at_n, 0)
|
||||||
|
|
||||||
|
const time_format string = "2006-01-02 15:04:05 -0700"
|
||||||
|
if now.Before(token.IssuedAt) {
|
||||||
|
return nil, fmt.Errorf("protected token not valid until %s", token.IssuedAt.Format(time_format))
|
||||||
|
}
|
||||||
|
if !now.Before(token.ExpiresAt) {
|
||||||
|
return nil, fmt.Errorf("protected token expired at %s", token.ExpiresAt.Format(time_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package hodu
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
import "crypto/rand"
|
||||||
|
import "crypto/rsa"
|
||||||
|
import "testing"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func TestRSAAESRoundTrip(t *testing.T) {
|
||||||
|
var key *rsa.PrivateKey
|
||||||
|
var codec *RSAAES
|
||||||
|
var input []byte
|
||||||
|
var doc string
|
||||||
|
var output []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate rsa key - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = NewRSAAES(key)
|
||||||
|
input = []byte("client-token-123")
|
||||||
|
|
||||||
|
doc, err = codec.Encipher(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to encipher - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err = codec.Decipher(doc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decipher - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(output, input) {
|
||||||
|
t.Fatalf("unexpected plaintext %q", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSAAESInvalidDocument(t *testing.T) {
|
||||||
|
var key *rsa.PrivateKey
|
||||||
|
var codec *RSAAES
|
||||||
|
var err error
|
||||||
|
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate rsa key - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = NewRSAAES(key)
|
||||||
|
_, err = codec.Decipher("broken")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected decipher failure for invalid document")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRSAAESTokenRoundTrip(t *testing.T) {
|
||||||
|
var key *rsa.PrivateKey
|
||||||
|
var codec *RSAAES
|
||||||
|
var issued_at time.Time
|
||||||
|
var expires_at time.Time
|
||||||
|
var doc string
|
||||||
|
var tok *RSAAESToken
|
||||||
|
var err error
|
||||||
|
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate rsa key - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
codec = NewRSAAES(key)
|
||||||
|
issued_at = time.Unix(1700000000, 0)
|
||||||
|
expires_at = issued_at.Add(30 * time.Second)
|
||||||
|
|
||||||
|
doc, err = codec.EncipherToken("client-token-xyz", issued_at, expires_at)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to encipher token - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err = codec.DecipherToken(doc, issued_at.Add(5 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decipher token - %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tok.Token != "client-token-xyz" {
|
||||||
|
t.Fatalf("unexpected token %q", tok.Token)
|
||||||
|
}
|
||||||
|
if !tok.IssuedAt.Equal(issued_at) {
|
||||||
|
t.Fatalf("unexpected issued-at %v", tok.IssuedAt)
|
||||||
|
}
|
||||||
|
if !tok.ExpiresAt.Equal(expires_at) {
|
||||||
|
t.Fatalf("unexpected expires-at %v", tok.ExpiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
-40
@@ -1,6 +1,7 @@
|
|||||||
package hodu
|
package hodu
|
||||||
|
|
||||||
import "container/list"
|
import "container/list"
|
||||||
|
import "crypto/rsa"
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "net/http"
|
import "net/http"
|
||||||
@@ -17,11 +18,15 @@ type ServerTokenClaim struct {
|
|||||||
IssuedAt int64 `json:"iat"`
|
IssuedAt int64 `json:"iat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type json_out_token struct {
|
type json_out_auth_token struct {
|
||||||
AccessToken string `json:"access-token"`
|
AccessToken string `json:"access-token"`
|
||||||
RefreshToken string `json:"refresh-token,omitempty"`
|
RefreshToken string `json:"refresh-token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type json_out_client_token struct {
|
||||||
|
ClientToken string `json:"client-token"`
|
||||||
|
}
|
||||||
|
|
||||||
type json_out_server_conn struct {
|
type json_out_server_conn struct {
|
||||||
CId ConnId `json:"conn-id"`
|
CId ConnId `json:"conn-id"`
|
||||||
ServerAddr string `json:"server-addr"`
|
ServerAddr string `json:"server-addr"`
|
||||||
@@ -96,6 +101,10 @@ type ServerCtl struct {
|
|||||||
NoAuth bool // override the auth configuration if true
|
NoAuth bool // override the auth configuration if true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type server_ctl_encipher struct {
|
||||||
|
ServerCtl
|
||||||
|
}
|
||||||
|
|
||||||
type server_ctl_token struct {
|
type server_ctl_token struct {
|
||||||
ServerCtl
|
ServerCtl
|
||||||
}
|
}
|
||||||
@@ -114,6 +123,7 @@ type server_ctl_server_conns_id_routes struct {
|
|||||||
|
|
||||||
type server_ctl_server_conns_id_routes_id struct {
|
type server_ctl_server_conns_id_routes_id struct {
|
||||||
ServerCtl
|
ServerCtl
|
||||||
|
HttpAuth *HttpAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type server_ctl_server_conns_id_routes_id_peers struct {
|
type server_ctl_server_conns_id_routes_id_peers struct {
|
||||||
@@ -164,48 +174,122 @@ func (ctl *ServerCtl) Cors(req *http.Request) bool {
|
|||||||
|
|
||||||
func (ctl *ServerCtl) Authenticate(req *http.Request) (int, string) {
|
func (ctl *ServerCtl) Authenticate(req *http.Request) (int, string) {
|
||||||
if ctl.NoAuth || ctl.S.Cfg.CtlAuth == nil { return http.StatusOK, "" }
|
if ctl.NoAuth || ctl.S.Cfg.CtlAuth == nil { return http.StatusOK, "" }
|
||||||
return ctl.S.Cfg.CtlAuth.Authenticate(req)
|
return ctl.S.Cfg.CtlAuth.Authenticate(req, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
func (ctl *server_ctl_token) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
func (ctl *server_ctl_token) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||||
var s *Server
|
var s *Server
|
||||||
|
var q url.Values
|
||||||
var status_code int
|
var status_code int
|
||||||
var je *json.Encoder
|
var je *json.Encoder
|
||||||
|
var _type string
|
||||||
|
var endpoint string
|
||||||
|
var source string
|
||||||
|
var key *rsa.PrivateKey
|
||||||
|
var ttl time.Duration
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
s = ctl.S
|
s = ctl.S
|
||||||
je = json.NewEncoder(w)
|
je = json.NewEncoder(w)
|
||||||
|
|
||||||
|
// while different combinations will return success
|
||||||
|
// meaningful combinations are as follows:
|
||||||
|
// - _ctl/token - get access token
|
||||||
|
// - _ctl/token?type=client-token&endpoint=rpx&source=abcdefg - get enciphered client token
|
||||||
|
// - _ctl/token?type=client-token&endpoint=rpty&source=abcdefg - get enciphered client token
|
||||||
|
q = req.URL.Query()
|
||||||
|
_type = q.Get("type")
|
||||||
|
endpoint = q.Get("endpoint")
|
||||||
|
source = q.Get("source")
|
||||||
|
|
||||||
|
if s.Cfg.CtlAuth == nil || !s.Cfg.CtlAuth.Enabled {
|
||||||
|
// this check may look a bit weird if endpoint is rpty or rpx
|
||||||
|
// but this request itself is coming in from the ctl endpoint
|
||||||
|
// if the ctl authentication is properly configured, i don't
|
||||||
|
// want to enable this call.
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusForbidden)
|
||||||
|
err = fmt.Errorf("auth not enabled")
|
||||||
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
|
goto oops
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint == "rpty" {
|
||||||
|
key = s.Cfg.RptyClientTokenRsaKey
|
||||||
|
ttl = s.Cfg.RptyClientTokenTtl
|
||||||
|
} else if endpoint == "rpx" {
|
||||||
|
key = s.Cfg.RpxClientTokenRsaKey
|
||||||
|
ttl = s.Cfg.RpxClientTokenTtl
|
||||||
|
} else {
|
||||||
|
key = s.Cfg.CtlAuth.TokenRsaKey
|
||||||
|
ttl = s.Cfg.CtlAuth.TokenTtl
|
||||||
|
}
|
||||||
|
_ = key
|
||||||
|
_ = ttl
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
var jwt *JWT[ServerTokenClaim]
|
if key == nil {
|
||||||
var claim ServerTokenClaim
|
|
||||||
var tok string
|
|
||||||
var now time.Time
|
|
||||||
|
|
||||||
if s.Cfg.CtlAuth == nil || !s.Cfg.CtlAuth.Enabled || s.Cfg.CtlAuth.TokenRsaKey == nil {
|
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusForbidden)
|
status_code = WriteJsonRespHeader(w, http.StatusForbidden)
|
||||||
err = fmt.Errorf("auth not enabled or token rsa key not set")
|
// 'enabled' may sound weird but use this word as it's given out tot the caller
|
||||||
|
err = fmt.Errorf("token rsa key not enabled")
|
||||||
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
|
goto oops
|
||||||
|
}
|
||||||
|
if ttl <= 0 {
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusForbidden)
|
||||||
|
// 'enabled' may sound weird but use this word as it's given out tot the caller
|
||||||
|
err = fmt.Errorf("token ttl not enabled")
|
||||||
je.Encode(JsonErrmsg{Text: err.Error()})
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
goto oops
|
goto oops
|
||||||
}
|
}
|
||||||
|
|
||||||
now = time.Now()
|
if _type == "client-token" {
|
||||||
claim.IssuedAt = now.Unix()
|
var enc *RSAAES
|
||||||
claim.ExpiresAt = now.Add(s.Cfg.CtlAuth.TokenTtl).Unix()
|
var tok string
|
||||||
jwt = NewJWT(s.Cfg.CtlAuth.TokenRsaKey, &claim)
|
var now time.Time
|
||||||
tok, err = jwt.SignRS512()
|
|
||||||
if err != nil {
|
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusInternalServerError)
|
|
||||||
je.Encode(JsonErrmsg{Text: err.Error()})
|
|
||||||
goto oops
|
|
||||||
}
|
|
||||||
|
|
||||||
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
if source == "" {
|
||||||
err = je.Encode(json_out_token{ AccessToken: tok }) // TODO: refresh token
|
status_code = WriteJsonRespHeader(w, http.StatusBadRequest)
|
||||||
if err != nil { goto oops }
|
err = fmt.Errorf("source text for client token not provided")
|
||||||
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
|
goto oops
|
||||||
|
}
|
||||||
|
|
||||||
|
enc = NewRSAAES(key)
|
||||||
|
now = time.Now()
|
||||||
|
tok, err = enc.EncipherToken(source, now, now.Add(ttl))
|
||||||
|
if err != nil {
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusInternalServerError)
|
||||||
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
|
goto oops
|
||||||
|
}
|
||||||
|
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
||||||
|
err = je.Encode(json_out_client_token{ ClientToken: tok })
|
||||||
|
if err != nil { goto oops }
|
||||||
|
} else {
|
||||||
|
var jwt *JWT[ServerTokenClaim]
|
||||||
|
var claim ServerTokenClaim
|
||||||
|
var tok string
|
||||||
|
var now time.Time
|
||||||
|
|
||||||
|
now = time.Now()
|
||||||
|
claim.IssuedAt = now.Unix()
|
||||||
|
claim.ExpiresAt = now.Add(ttl).Unix()
|
||||||
|
jwt = NewJWT(key, &claim)
|
||||||
|
tok, err = jwt.SignRS256()
|
||||||
|
if err != nil {
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusInternalServerError)
|
||||||
|
je.Encode(JsonErrmsg{Text: err.Error()})
|
||||||
|
goto oops
|
||||||
|
}
|
||||||
|
|
||||||
|
status_code = WriteJsonRespHeader(w, http.StatusOK)
|
||||||
|
err = je.Encode(json_out_auth_token{ AccessToken: tok }) // TODO: refresh token
|
||||||
|
if err != nil { goto oops }
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusMethodNotAllowed)
|
status_code = WriteEmptyRespHeader(w, http.StatusMethodNotAllowed)
|
||||||
@@ -447,6 +531,18 @@ oops:
|
|||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
|
||||||
|
func (ctl *server_ctl_server_conns_id_routes_id) Authenticate(req *http.Request) (int, string) {
|
||||||
|
if ctl.HttpAuth != nil {
|
||||||
|
// this is kind of hack to cater for the use of this object
|
||||||
|
// from the wpx context for session-info call
|
||||||
|
return ctl.HttpAuth.Authenticate(req, "access-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// this part must be the same as ServerCtl.Authenticate
|
||||||
|
if ctl.NoAuth || ctl.S.Cfg.CtlAuth == nil { return http.StatusOK, "" }
|
||||||
|
return ctl.S.Cfg.CtlAuth.Authenticate(req, "")
|
||||||
|
}
|
||||||
|
|
||||||
func (ctl *server_ctl_server_conns_id_routes_id) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
func (ctl *server_ctl_server_conns_id_routes_id) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||||
var s *Server
|
var s *Server
|
||||||
var status_code int
|
var status_code int
|
||||||
@@ -956,24 +1052,8 @@ func (ctl *server_ctl_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
// handle authentication using the first message.
|
// handle authentication using the first message.
|
||||||
// end this task if authentication fails.
|
// end this task if authentication fails.
|
||||||
if !ctl.NoAuth && s.Cfg.CtlAuth != nil {
|
if !ctl.NoAuth && s.Cfg.CtlAuth != nil {
|
||||||
var req *http.Request
|
status_code, _ = s.Cfg.CtlAuth.Authenticate(ws.Request(), "access-token")
|
||||||
|
if status_code != http.StatusOK { goto done }
|
||||||
req = ws.Request()
|
|
||||||
if req.Header.Get("Authorization") == "" {
|
|
||||||
var token string
|
|
||||||
token = req.FormValue("token")
|
|
||||||
if token != "" {
|
|
||||||
// websocket doesn't actual have extra headers except a few fixed
|
|
||||||
// ones. add "Authorization" header from the query paramerer and
|
|
||||||
// compose a fake header to reuse the same Authentication() function
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status_code, _ = s.Cfg.CtlAuth.Authenticate(req)
|
|
||||||
if status_code != http.StatusOK {
|
|
||||||
goto done
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sbsc, err = s.bulletin.Subscribe("")
|
sbsc, err = s.bulletin.Subscribe("")
|
||||||
|
|||||||
+106
-6
@@ -21,12 +21,14 @@ import "golang.org/x/sys/unix"
|
|||||||
type server_pty_ws struct {
|
type server_pty_ws struct {
|
||||||
S *Server
|
S *Server
|
||||||
Id string
|
Id string
|
||||||
|
Auth *HttpAuthConfig
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
type server_rpty_ws struct {
|
type server_rpty_ws struct {
|
||||||
S *Server
|
S *Server
|
||||||
Id string
|
Id string
|
||||||
|
Auth *HttpAuthConfig
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ type server_pty_xterm_file struct {
|
|||||||
ServerCtl
|
ServerCtl
|
||||||
file string
|
file string
|
||||||
mode string
|
mode string
|
||||||
|
auth *HttpAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -45,6 +48,7 @@ func (pty *server_pty_ws) Identity() string {
|
|||||||
func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
||||||
var s *Server
|
var s *Server
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
var auth *HttpAuthConfig
|
||||||
//var username string
|
//var username string
|
||||||
//var password string
|
//var password string
|
||||||
var in *os.File
|
var in *os.File
|
||||||
@@ -58,6 +62,20 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
|
|
||||||
s = pty.S
|
s = pty.S
|
||||||
req = ws.Request()
|
req = ws.Request()
|
||||||
|
|
||||||
|
// handle authentication using the first message.
|
||||||
|
// end this task if authentication fails.
|
||||||
|
auth = pty.Auth
|
||||||
|
if auth == nil { auth = s.Cfg.CtlAuth }
|
||||||
|
if auth != nil {
|
||||||
|
var status_code int
|
||||||
|
status_code, _ = auth.Authenticate(req, "access-token")
|
||||||
|
if status_code != http.StatusOK {
|
||||||
|
ws.Close()
|
||||||
|
return status_code, fmt.Errorf("failed to authenticate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn_ready_chan = make(chan bool, 3)
|
conn_ready_chan = make(chan bool, 3)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@@ -78,8 +96,8 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
poll_fds = []unix.PollFd{
|
poll_fds = []unix.PollFd{
|
||||||
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
|
|
||||||
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
|
unix.PollFd{Fd: int32(pfd[0]), Events: unix.POLLIN},
|
||||||
|
unix.PollFd{Fd: int32(out.Fd()), Events: unix.POLLIN},
|
||||||
}
|
}
|
||||||
|
|
||||||
s.stats.pty_sessions.Add(1)
|
s.stats.pty_sessions.Add(1)
|
||||||
@@ -94,8 +112,8 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
out_revents = poll_fds[0].Revents
|
sig_revents = poll_fds[0].Revents
|
||||||
sig_revents = poll_fds[1].Revents
|
out_revents = poll_fds[1].Revents
|
||||||
|
|
||||||
if (out_revents & unix.POLLIN) != 0 {
|
if (out_revents & unix.POLLIN) != 0 {
|
||||||
n, err = out.Read(buf[:])
|
n, err = out.Read(buf[:])
|
||||||
@@ -114,7 +132,27 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sig_revents & unix.POLLIN) != 0 {
|
if (sig_revents & unix.POLLIN) != 0 {
|
||||||
|
// currently the code doesn't read from the signal fd as the only
|
||||||
|
// implemented signal is stop. for now it exits the loop.
|
||||||
|
/*
|
||||||
|
// read the signal fd
|
||||||
|
n, err = unix.Read(pfd[0], buf[0:1]) // consume the 1-byte header. signal string max 255 chars
|
||||||
|
if (err == nil && n > 0) {
|
||||||
|
err = read_fd_full(pfd[0], buf[0:n]) // consume n bytes
|
||||||
|
if (err == nil) {
|
||||||
|
// new pty session requested
|
||||||
|
if buf[0] == 'o' {
|
||||||
|
poll_fds = append(poll_fds,
|
||||||
|
} else if buf[0] == 'c' {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// treat read failure as stop notification
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Stop request noticed on pty signal pipe", req.RemoteAddr)
|
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Stop request noticed on pty signal pipe", req.RemoteAddr)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -123,6 +161,7 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Error detected on pty stdout", req.RemoteAddr)
|
s.log.Write(pty.Id, LOG_DEBUG, "[%s] Error detected on pty stdout", req.RemoteAddr)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sig_revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
|
if (sig_revents & (unix.POLLERR | unix.POLLHUP | unix.POLLNVAL)) != 0 {
|
||||||
s.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty signal pipe", req.RemoteAddr)
|
s.log.Write(pty.Id, LOG_DEBUG, "[%s] EOF detected on pty signal pipe", req.RemoteAddr)
|
||||||
break
|
break
|
||||||
@@ -132,6 +171,7 @@ func (pty *server_pty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.stats.pty_sessions.Add(-1)
|
s.stats.pty_sessions.Add(-1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -205,6 +245,7 @@ ws_recv_loop:
|
|||||||
tty = nil
|
tty = nil
|
||||||
}
|
}
|
||||||
if pfd[1] >= 0 {
|
if pfd[1] >= 0 {
|
||||||
|
// write to the signal fd
|
||||||
unix.Write(pfd[1], []byte{0})
|
unix.Write(pfd[1], []byte{0})
|
||||||
}
|
}
|
||||||
break ws_recv_loop
|
break ws_recv_loop
|
||||||
@@ -280,6 +321,7 @@ func (rpty *server_rpty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
var req *http.Request
|
var req *http.Request
|
||||||
var token string
|
var token string
|
||||||
var cts *ServerConn
|
var cts *ServerConn
|
||||||
|
var auth *HttpAuthConfig
|
||||||
//var username string
|
//var username string
|
||||||
//var password string
|
//var password string
|
||||||
var rp *ServerRpty
|
var rp *ServerRpty
|
||||||
@@ -288,16 +330,48 @@ func (rpty *server_rpty_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
|
|
||||||
s = rpty.S
|
s = rpty.S
|
||||||
req = ws.Request()
|
req = ws.Request()
|
||||||
|
|
||||||
|
// handle authentication using the first message.
|
||||||
|
// end this task if authentication fails.
|
||||||
|
auth = rpty.Auth
|
||||||
|
if auth == nil { auth = s.Cfg.CtlAuth }
|
||||||
|
if auth != nil {
|
||||||
|
var status_code int
|
||||||
|
status_code, _ = auth.Authenticate(req, "access-token")
|
||||||
|
if status_code != http.StatusOK {
|
||||||
|
ws.Close()
|
||||||
|
return status_code, fmt.Errorf("failed to authenticate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
token = req.FormValue("client-token")
|
token = req.FormValue("client-token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
ws.Close()
|
ws.Close()
|
||||||
return http.StatusBadRequest, fmt.Errorf("no client token specified")
|
return http.StatusBadRequest, fmt.Errorf("no client token specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(s.Cfg.RptyClientTokenProtection, "rsa-aes-256-gcm") {
|
||||||
|
var rsa_aes *RSAAES
|
||||||
|
var rsa_token *RSAAESToken
|
||||||
|
|
||||||
|
if s.Cfg.RptyClientTokenRsaKey == nil {
|
||||||
|
s.log.Write(rpty.Id, LOG_WARN, "[%s] Failed to decrypt protected client token [%s] - no rpty client token rsa key for %s", req.RemoteAddr, token, s.Cfg.RptyClientTokenProtection)
|
||||||
|
return http.StatusInternalServerError, fmt.Errorf("client token decryption failure - %s", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsa_aes = NewRSAAES(s.Cfg.RptyClientTokenRsaKey)
|
||||||
|
rsa_token, err = rsa_aes.DecipherToken(token, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
s.log.Write(rpty.Id, LOG_WARN, "[%s] Failed to decrypt protected client token [%s] - %s", req.RemoteAddr, token, err.Error())
|
||||||
|
return http.StatusBadRequest, fmt.Errorf("invalid client token - %s", token)
|
||||||
|
}
|
||||||
|
token = rsa_token.Token
|
||||||
|
}
|
||||||
|
|
||||||
cts = s.FindServerConnByClientToken(token)
|
cts = s.FindServerConnByClientToken(token)
|
||||||
if cts == nil {
|
if cts == nil {
|
||||||
ws.Close()
|
ws.Close()
|
||||||
return http.StatusBadRequest, fmt.Errorf("invalid client token - %s", token)
|
return http.StatusBadRequest, fmt.Errorf("client token not found - %s", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
ws_recv_loop:
|
ws_recv_loop:
|
||||||
@@ -381,6 +455,30 @@ done:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
|
//Originally, i used a plain xterm.html and use server_pty_xterm_file to serve
|
||||||
|
//other files that xterm.html loads. Now, i bundle everything into xterm.html
|
||||||
|
//so no other files need to be served
|
||||||
|
|
||||||
|
func (pty *server_pty_xterm_file) Authenticate(req *http.Request) (int, string) {
|
||||||
|
// The parent method ServerCtl.Authenticate() applies
|
||||||
|
// authentication to all resources. i want to exclude some files.
|
||||||
|
var auth *HttpAuthConfig
|
||||||
|
|
||||||
|
auth = pty.auth
|
||||||
|
if auth == nil { auth = pty.S.Cfg.CtlAuth }
|
||||||
|
if auth != nil && pty.file == "xterm.html" {
|
||||||
|
// this is not a real api call. but at least for xterm.html,
|
||||||
|
// i don't bypass authentication and and in addition,
|
||||||
|
// i check the value of the "access-token" parameter for
|
||||||
|
// jwt authentication if it exists
|
||||||
|
return auth.Authenticate(req, "access-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// you can download other files without authentication
|
||||||
|
// even if authentication is enabled. but this part must not be reached
|
||||||
|
// because xterm.html is the only meanful resource as of now
|
||||||
|
return http.StatusOK, ""
|
||||||
|
}
|
||||||
|
|
||||||
func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) (int, error) {
|
||||||
var s *Server
|
var s *Server
|
||||||
@@ -390,6 +488,7 @@ func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
s = pty.S
|
s = pty.S
|
||||||
|
|
||||||
switch pty.file {
|
switch pty.file {
|
||||||
|
/*
|
||||||
case "xterm.js":
|
case "xterm.js":
|
||||||
status_code = WriteJsRespHeader(w, http.StatusOK)
|
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_js)
|
w.Write(xterm_js)
|
||||||
@@ -402,10 +501,11 @@ func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
case "xterm.css":
|
case "xterm.css":
|
||||||
status_code = WriteCssRespHeader(w, http.StatusOK)
|
status_code = WriteCssRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_css)
|
w.Write(xterm_css)
|
||||||
|
*/
|
||||||
case "xterm.html":
|
case "xterm.html":
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
|
|
||||||
tmpl = template.New("")
|
tmpl = template.New("").Delims("{{@@", "@@}}")
|
||||||
if s.xterm_html != "" {
|
if s.xterm_html != "" {
|
||||||
_, err = tmpl.Parse(s.xterm_html)
|
_, err = tmpl.Parse(s.xterm_html)
|
||||||
} else {
|
} else {
|
||||||
@@ -433,7 +533,7 @@ func (pty *server_pty_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
default:
|
default:
|
||||||
if strings.HasPrefix(pty.file, "_redir:") {
|
if strings.HasPrefix(pty.file, "_redir:") {
|
||||||
status_code = http.StatusMovedPermanently
|
status_code = http.StatusMovedPermanently
|
||||||
w.Header().Set("Location", pty.file[7:])
|
w.Header().Set("Location", append_raw_query(pty.file[7:], req))
|
||||||
w.WriteHeader(status_code)
|
w.WriteHeader(status_code)
|
||||||
} else {
|
} else {
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
|
|||||||
+24
-3
@@ -35,6 +35,7 @@ type server_pxy_http_main struct {
|
|||||||
type server_pxy_xterm_file struct {
|
type server_pxy_xterm_file struct {
|
||||||
server_pxy
|
server_pxy
|
||||||
file string
|
file string
|
||||||
|
HttpAuth* HttpAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type server_pxy_http_wpx struct {
|
type server_pxy_http_wpx struct {
|
||||||
@@ -187,6 +188,7 @@ func (pxy *server_pxy) Cors(req *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *server_pxy) Authenticate(req *http.Request) (int, string) {
|
func (pxy *server_pxy) Authenticate(req *http.Request) (int, string) {
|
||||||
|
// no authentication by default
|
||||||
return http.StatusOK, ""
|
return http.StatusOK, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +466,12 @@ func (pxy *server_pxy_http_wpx) ServeHTTP(w http.ResponseWriter, req *http.Reque
|
|||||||
// return status_code, err
|
// return status_code, err
|
||||||
}
|
}
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
func (pxy *server_pxy_xterm_file) Authenticate(req *http.Request) (int, string) {
|
||||||
|
if pxy.HttpAuth != nil && pxy.file == "xterm.html" {
|
||||||
|
return pxy.HttpAuth.Authenticate(req, "access-token")
|
||||||
|
}
|
||||||
|
return http.StatusOK, ""
|
||||||
|
}
|
||||||
|
|
||||||
func (pxy *server_pxy_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 s *Server
|
||||||
@@ -473,6 +481,7 @@ func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
s = pxy.S
|
s = pxy.S
|
||||||
|
|
||||||
switch pxy.file {
|
switch pxy.file {
|
||||||
|
/*
|
||||||
case "xterm.js":
|
case "xterm.js":
|
||||||
status_code = WriteJsRespHeader(w, http.StatusOK)
|
status_code = WriteJsRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_js)
|
w.Write(xterm_js)
|
||||||
@@ -485,6 +494,7 @@ func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
case "xterm.css":
|
case "xterm.css":
|
||||||
status_code = WriteCssRespHeader(w, http.StatusOK)
|
status_code = WriteCssRespHeader(w, http.StatusOK)
|
||||||
w.Write(xterm_css)
|
w.Write(xterm_css)
|
||||||
|
*/
|
||||||
case "xterm.html":
|
case "xterm.html":
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
var conn_id string
|
var conn_id string
|
||||||
@@ -509,7 +519,7 @@ func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
goto oops
|
goto oops
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl = template.New("")
|
tmpl = template.New("").Delims("{{@@", "@@}}")
|
||||||
if s.xterm_html != "" {
|
if s.xterm_html != "" {
|
||||||
_, err = tmpl.Parse(s.xterm_html)
|
_, err = tmpl.Parse(s.xterm_html)
|
||||||
} else {
|
} else {
|
||||||
@@ -544,7 +554,7 @@ func (pxy *server_pxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Req
|
|||||||
default:
|
default:
|
||||||
if strings.HasPrefix(pxy.file, "_redir:") {
|
if strings.HasPrefix(pxy.file, "_redir:") {
|
||||||
status_code = http.StatusMovedPermanently
|
status_code = http.StatusMovedPermanently
|
||||||
w.Header().Set("Location", pxy.file[7:])
|
w.Header().Set("Location", append_raw_query(pxy.file[7:], req))
|
||||||
w.WriteHeader(status_code)
|
w.WriteHeader(status_code)
|
||||||
} else {
|
} else {
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
@@ -664,6 +674,7 @@ type server_pxy_ssh_ws struct {
|
|||||||
S *Server
|
S *Server
|
||||||
ws *websocket.Conn
|
ws *websocket.Conn
|
||||||
Id string
|
Id string
|
||||||
|
HttpAuth* HttpAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *server_pxy_ssh_ws) Identity() string {
|
func (pxy *server_pxy_ssh_ws) Identity() string {
|
||||||
@@ -839,8 +850,18 @@ func (pxy *server_pxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
|
|
||||||
s = pxy.S
|
s = pxy.S
|
||||||
req = ws.Request()
|
req = ws.Request()
|
||||||
|
|
||||||
ssh_state.init()
|
ssh_state.init()
|
||||||
|
|
||||||
|
// server_pxy_ssh_ws is instantiated from two different contexts - pxy and wpx
|
||||||
|
// use the provided HttpAuth value for each instantiation rather than accessing
|
||||||
|
// the server configuration directly.
|
||||||
|
if pxy.HttpAuth != nil {
|
||||||
|
var status_code int
|
||||||
|
status_code, _ = pxy.HttpAuth.Authenticate(ws.Request(), "access-token")
|
||||||
|
if status_code != http.StatusOK { goto done }
|
||||||
|
}
|
||||||
|
|
||||||
port_id = req.PathValue("port_id")
|
port_id = req.PathValue("port_id")
|
||||||
conn_id = req.PathValue("conn_id")
|
conn_id = req.PathValue("conn_id")
|
||||||
route_id = req.PathValue("route_id")
|
route_id = req.PathValue("route_id")
|
||||||
@@ -860,7 +881,7 @@ func (pxy *server_pxy_ssh_ws) ServeWebsocket(ws *websocket.Conn) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// [SUPER-IMPORTANT!!]
|
// [SUPER-IMPORTANT!!]
|
||||||
// create a fake server route. this is not a compleete structure.
|
// create a fake server route. this is not a complete structure.
|
||||||
// some pointer fields are nil. extra care needs to be taken
|
// some pointer fields are nil. extra care needs to be taken
|
||||||
// below to ensure it doesn't access undesired fields when exitending
|
// below to ensure it doesn't access undesired fields when exitending
|
||||||
// code further
|
// code further
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import "net/http"
|
|||||||
import "strconv"
|
import "strconv"
|
||||||
import "strings"
|
import "strings"
|
||||||
import "sync"
|
import "sync"
|
||||||
|
import "time"
|
||||||
|
|
||||||
type server_rpx struct {
|
type server_rpx struct {
|
||||||
S *Server
|
S *Server
|
||||||
@@ -31,6 +32,9 @@ func (rpx *server_rpx) Authenticate(req *http.Request) (int, string) {
|
|||||||
|
|
||||||
func (rpx *server_rpx) get_client_token(req *http.Request) string {
|
func (rpx *server_rpx) get_client_token(req *http.Request) string {
|
||||||
var val string
|
var val string
|
||||||
|
var rsa_aes *RSAAES
|
||||||
|
var token *RSAAESToken
|
||||||
|
var err error
|
||||||
|
|
||||||
// TODO: enhance this client token extraction logic with some expression language?
|
// TODO: enhance this client token extraction logic with some expression language?
|
||||||
val = req.Header.Get(rpx.S.Cfg.RpxClientTokenAttrName)
|
val = req.Header.Get(rpx.S.Cfg.RpxClientTokenAttrName)
|
||||||
@@ -40,6 +44,21 @@ func (rpx *server_rpx) get_client_token(req *http.Request) string {
|
|||||||
val = get_regex_submatch(rpx.S.Cfg.RpxClientTokenRegex, val, rpx.S.Cfg.RpxClientTokenSubmatchIndex)
|
val = get_regex_submatch(rpx.S.Cfg.RpxClientTokenRegex, val, rpx.S.Cfg.RpxClientTokenSubmatchIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(rpx.S.Cfg.RpxClientTokenProtection, "rsa-aes-256-gcm") {
|
||||||
|
if rpx.S.Cfg.RpxClientTokenRsaKey == nil {
|
||||||
|
rpx.S.log.Write(rpx.Id, LOG_WARN, "[%s] Failed to decrypt protected client token [%s] - no rpx client token rsa key for %s", req.RemoteAddr, val, rpx.S.Cfg.RpxClientTokenProtection)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
rsa_aes = NewRSAAES(rpx.S.Cfg.RpxClientTokenRsaKey)
|
||||||
|
token, err = rsa_aes.DecipherToken(val, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
rpx.S.log.Write(rpx.Id, LOG_WARN, "[%s] Failed to decrypt protected client token [%s] - %s", req.RemoteAddr, val, err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val = token.Token
|
||||||
|
}
|
||||||
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +69,7 @@ func (rpx* server_rpx) handle_header_data(rpx_id uint64, data []byte, w http.Res
|
|||||||
var status_code int
|
var status_code int
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// i don't want the sender hammer the server with very large header data
|
||||||
const rpx_header_line_max = 65535 // TODO: make this configurable
|
const rpx_header_line_max = 65535 // TODO: make this configurable
|
||||||
|
|
||||||
r = bufio.NewReader(bytes.NewReader(data))
|
r = bufio.NewReader(bytes.NewReader(data))
|
||||||
@@ -231,10 +251,12 @@ func (rpx *server_rpx) ServeHTTP(w http.ResponseWriter, req *http.Request) (int,
|
|||||||
var ws_upgrade bool
|
var ws_upgrade bool
|
||||||
var buf [4096]byte
|
var buf [4096]byte
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
var xinfo *HttpHandlerExtraInfo
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
s = rpx.S
|
s = rpx.S
|
||||||
client_token = rpx.get_client_token(req)
|
client_token = rpx.get_client_token(req)
|
||||||
|
|
||||||
cts = s.FindServerConnByClientToken(client_token)
|
cts = s.FindServerConnByClientToken(client_token)
|
||||||
if cts == nil {
|
if cts == nil {
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
status_code = WriteEmptyRespHeader(w, http.StatusNotFound)
|
||||||
@@ -242,6 +264,12 @@ func (rpx *server_rpx) ServeHTTP(w http.ResponseWriter, req *http.Request) (int,
|
|||||||
goto oops
|
goto oops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xinfo = req.Context().Value(http_handler_extra_info_key).(*HttpHandlerExtraInfo)
|
||||||
|
if xinfo != nil {
|
||||||
|
// set more info for logging in the outer handler
|
||||||
|
xinfo.extra_id = client_token
|
||||||
|
}
|
||||||
|
|
||||||
srpx, err = rpx.alloc_server_rpx(cts, req)
|
srpx, err = rpx.alloc_server_rpx(cts, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status_code = WriteEmptyRespHeader(w, http.StatusServiceUnavailable)
|
status_code = WriteEmptyRespHeader(w, http.StatusServiceUnavailable)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hodu
|
|||||||
|
|
||||||
import "container/list"
|
import "container/list"
|
||||||
import "context"
|
import "context"
|
||||||
|
import "crypto/rsa"
|
||||||
import "crypto/tls"
|
import "crypto/tls"
|
||||||
import "errors"
|
import "errors"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
@@ -34,6 +35,7 @@ const CTS_LIMIT int = 16384
|
|||||||
type PortId uint16
|
type PortId uint16
|
||||||
const PORT_ID_MARKER string = "_"
|
const PORT_ID_MARKER string = "_"
|
||||||
const HS_ID_CTL string = "ctl"
|
const HS_ID_CTL string = "ctl"
|
||||||
|
const HS_ID_ECT string = "ect"
|
||||||
const HS_ID_RPX string = "rpx"
|
const HS_ID_RPX string = "rpx"
|
||||||
const HS_ID_PXY string = "pxy"
|
const HS_ID_PXY string = "pxy"
|
||||||
const HS_ID_WPX string = "wpx"
|
const HS_ID_WPX string = "wpx"
|
||||||
@@ -79,18 +81,31 @@ type ServerConfig struct {
|
|||||||
CtlAuth *HttpAuthConfig
|
CtlAuth *HttpAuthConfig
|
||||||
CtlCors bool
|
CtlCors bool
|
||||||
|
|
||||||
|
EctAddrs []string
|
||||||
|
EctTls *tls.Config
|
||||||
|
EctAuth *HttpAuthConfig
|
||||||
|
|
||||||
|
RptyClientTokenProtection string
|
||||||
|
RptyClientTokenRsaKey *rsa.PrivateKey
|
||||||
|
RptyClientTokenTtl time.Duration
|
||||||
|
|
||||||
RpxAddrs []string
|
RpxAddrs []string
|
||||||
RpxTls *tls.Config
|
RpxTls *tls.Config
|
||||||
RpxClientTokenAttrName string
|
RpxClientTokenAttrName string
|
||||||
|
RpxClientTokenProtection string
|
||||||
|
RpxClientTokenRsaKey *rsa.PrivateKey
|
||||||
RpxClientTokenRegex *regexp.Regexp
|
RpxClientTokenRegex *regexp.Regexp
|
||||||
RpxClientTokenSubmatchIndex int
|
RpxClientTokenSubmatchIndex int
|
||||||
|
RpxClientTokenTtl time.Duration
|
||||||
|
|
||||||
PxyAddrs []string
|
PxyAddrs []string
|
||||||
PxyTls *tls.Config
|
PxyTls *tls.Config
|
||||||
PxyTargetTls *tls.Config
|
PxyTargetTls *tls.Config
|
||||||
|
PxyAuth *HttpAuthConfig
|
||||||
|
|
||||||
WpxAddrs []string
|
WpxAddrs []string
|
||||||
WpxTls *tls.Config
|
WpxTls *tls.Config
|
||||||
|
WpxAuth *HttpAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerEventKind int
|
type ServerEventKind int
|
||||||
@@ -152,6 +167,11 @@ type Server struct {
|
|||||||
ctl_addrs_mtx sync.Mutex
|
ctl_addrs_mtx sync.Mutex
|
||||||
ctl_addrs *list.List // of net.Addr
|
ctl_addrs *list.List // of net.Addr
|
||||||
|
|
||||||
|
ect_mux *http.ServeMux
|
||||||
|
ect []*http.Server // control server
|
||||||
|
ect_addrs_mtx sync.Mutex
|
||||||
|
ect_addrs *list.List // of net.Addr
|
||||||
|
|
||||||
rpc []*net.TCPListener // main listener for grpc
|
rpc []*net.TCPListener // main listener for grpc
|
||||||
rpc_wg sync.WaitGroup
|
rpc_wg sync.WaitGroup
|
||||||
rpc_svr *grpc.Server
|
rpc_svr *grpc.Server
|
||||||
@@ -1381,7 +1401,7 @@ func (hlw *server_http_log_writer) Write(p []byte) (n int, err error) {
|
|||||||
// the standard http.Server always requires *log.Logger
|
// the standard http.Server always requires *log.Logger
|
||||||
// use this iowriter to create a logger to pass it to the http server.
|
// use this iowriter to create a logger to pass it to the http server.
|
||||||
// since this is another log write wrapper, give adjustment value
|
// since this is another log write wrapper, give adjustment value
|
||||||
hlw.svr.log.WriteWithCallDepth(hlw.id, LOG_INFO, hlw.depth, string(p))
|
hlw.svr.log.WriteWithCallDepth(hlw.id, LOG_INFO, hlw.depth, "%s", string(p))
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1403,6 +1423,8 @@ func (s *Server) WrapHttpHandler(handler ServerHttpHandler) http.Handler {
|
|||||||
var err error
|
var err error
|
||||||
var start_time time.Time
|
var start_time time.Time
|
||||||
var time_taken time.Duration
|
var time_taken time.Duration
|
||||||
|
var newctx context.Context
|
||||||
|
var xinfo HttpHandlerExtraInfo
|
||||||
|
|
||||||
// this deferred function is to overcome the recovering implemenation
|
// this deferred function is to overcome the recovering implemenation
|
||||||
// from panic done in go's http server. in that implemenation, panic
|
// from panic done in go's http server. in that implemenation, panic
|
||||||
@@ -1416,6 +1438,9 @@ func (s *Server) WrapHttpHandler(handler ServerHttpHandler) http.Handler {
|
|||||||
|
|
||||||
start_time = time.Now()
|
start_time = time.Now()
|
||||||
|
|
||||||
|
newctx = context.WithValue(req.Context(), http_handler_extra_info_key, &xinfo)
|
||||||
|
req = req.WithContext(newctx)
|
||||||
|
|
||||||
if handler.Cors(req) {
|
if handler.Cors(req) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||||
@@ -1426,6 +1451,7 @@ func (s *Server) WrapHttpHandler(handler ServerHttpHandler) http.Handler {
|
|||||||
} else {
|
} else {
|
||||||
var realm string
|
var realm string
|
||||||
|
|
||||||
|
// authorization applies to a controll with proper Authenticate method override.
|
||||||
status_code, realm = handler.Authenticate(req)
|
status_code, realm = handler.Authenticate(req)
|
||||||
if status_code == http.StatusUnauthorized {
|
if status_code == http.StatusUnauthorized {
|
||||||
if realm != "" {
|
if realm != "" {
|
||||||
@@ -1441,10 +1467,12 @@ func (s *Server) WrapHttpHandler(handler ServerHttpHandler) http.Handler {
|
|||||||
time_taken = time.Since(start_time) // time.Now().Sub(start_time)
|
time_taken = time.Since(start_time) // time.Now().Sub(start_time)
|
||||||
|
|
||||||
if status_code > 0 {
|
if status_code > 0 {
|
||||||
|
var id string = handler.Identity()
|
||||||
|
if xinfo.extra_id != "" { id = id + "/" + xinfo.extra_id }
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Write(handler.Identity(), LOG_INFO, "[%s] %s %s %d %.9f - %s", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds(), err.Error())
|
s.log.Write(id, LOG_INFO, "[%s] %s %s %d %.9f - %s", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds(), err.Error())
|
||||||
} else {
|
} else {
|
||||||
s.log.Write(handler.Identity(), LOG_INFO, "[%s] %s %s %d %.9f", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds())
|
s.log.Write(id, LOG_INFO, "[%s] %s %s %d %.9f", req.RemoteAddr, req.Method, req.RequestURI, status_code, time_taken.Seconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1464,7 +1492,9 @@ func (s *Server) SafeWrapWebsocketHandler(handler websocket.Handler) http.Handle
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) WrapWebsocketHandler(handler ServerWebsocketHandler) websocket.Handler {
|
func (s *Server) WrapWebsocketHandler(handler ServerWebsocketHandler) websocket.Handler {
|
||||||
return websocket.Handler(func(ws *websocket.Conn) {
|
var f websocket.Handler
|
||||||
|
|
||||||
|
f = websocket.Handler(func(ws *websocket.Conn) {
|
||||||
var status_code int
|
var status_code int
|
||||||
var err error
|
var err error
|
||||||
var start_time time.Time
|
var start_time time.Time
|
||||||
@@ -1486,6 +1516,7 @@ func (s *Server) WrapWebsocketHandler(handler ServerWebsocketHandler) websocket.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfig) (*Server, error) {
|
func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfig) (*Server, error) {
|
||||||
@@ -1499,6 +1530,7 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
var hs_max_header_bytes int
|
var hs_max_header_bytes int
|
||||||
var hs_base_ctx func(net.Listener) context.Context
|
var hs_base_ctx func(net.Listener) context.Context
|
||||||
var hs_log_ctl *log.Logger
|
var hs_log_ctl *log.Logger
|
||||||
|
var hs_log_ect *log.Logger
|
||||||
var hs_log_rpx *log.Logger
|
var hs_log_rpx *log.Logger
|
||||||
var hs_log_pxy *log.Logger
|
var hs_log_pxy *log.Logger
|
||||||
var hs_log_wpx *log.Logger
|
var hs_log_wpx *log.Logger
|
||||||
@@ -1547,6 +1579,7 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
s.bulletin = NewBulletin[*ServerEvent](&s, 1024)
|
s.bulletin = NewBulletin[*ServerEvent](&s, 1024)
|
||||||
|
|
||||||
s.ctl_addrs = list.New()
|
s.ctl_addrs = list.New()
|
||||||
|
s.ect_addrs = list.New()
|
||||||
s.rpx_addrs = list.New()
|
s.rpx_addrs = list.New()
|
||||||
s.pxy_addrs = list.New()
|
s.pxy_addrs = list.New()
|
||||||
s.wpx_addrs = list.New()
|
s.wpx_addrs = list.New()
|
||||||
@@ -1576,10 +1609,11 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
// if s.Ctx is cancelled, i like all in-flight requests to be cancelled as well
|
// if s.Ctx is cancelled, i like all in-flight requests to be cancelled as well
|
||||||
hs_base_ctx = func(_ net.Listener) context.Context { return s.Ctx }
|
hs_base_ctx = func(_ net.Listener) context.Context { return s.Ctx }
|
||||||
|
|
||||||
hs_log_ctl = log.New(&server_http_log_writer{svr: &s, id: "ctl", depth: +2}, "", 0)
|
hs_log_ctl = log.New(&server_http_log_writer{svr: &s, id: HS_ID_CTL, depth: +2}, "", 0)
|
||||||
hs_log_rpx = log.New(&server_http_log_writer{svr: &s, id: "rpx", depth: +2}, "", 0)
|
hs_log_ect = log.New(&server_http_log_writer{svr: &s, id: HS_ID_ECT, depth: +2}, "", 0)
|
||||||
hs_log_pxy = log.New(&server_http_log_writer{svr: &s, id: "pxy", depth: +2}, "", 0)
|
hs_log_rpx = log.New(&server_http_log_writer{svr: &s, id: HS_ID_RPX, depth: +2}, "", 0)
|
||||||
hs_log_wpx = log.New(&server_http_log_writer{svr: &s, id: "wpx", depth: +2}, "", 0)
|
hs_log_pxy = log.New(&server_http_log_writer{svr: &s, id: HS_ID_PXY, depth: +2}, "", 0)
|
||||||
|
hs_log_wpx = log.New(&server_http_log_writer{svr: &s, id: HS_ID_WPX, depth: +2}, "", 0)
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -1592,7 +1626,8 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes{ServerCtl{S: &s, Id: HS_ID_CTL}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes{ServerCtl{S: &s, Id: HS_ID_CTL}}))
|
||||||
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_CTL}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, HttpAuth: nil}))
|
||||||
|
|
||||||
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}/peers",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}/peers",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id_peers{ServerCtl{S: &s, Id: HS_ID_CTL}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id_peers{ServerCtl{S: &s, Id: HS_ID_CTL}}))
|
||||||
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}/peers/{peer_id}",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/server-conns/{conn_id}/routes/{route_id}/peers/{peer_id}",
|
||||||
@@ -1619,57 +1654,29 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/metrics",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/metrics",
|
||||||
promhttp.HandlerFor(s.promreg, promhttp.HandlerOpts{ EnableOpenMetrics: true }))
|
promhttp.HandlerFor(s.promreg, promhttp.HandlerOpts{ EnableOpenMetrics: true }))
|
||||||
|
|
||||||
s.ctl_mux.Handle("/_ctl/events",
|
s.ctl_mux.Handle(s.Cfg.CtlPrefix + "/_ctl/events",
|
||||||
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_ctl_ws{ServerCtl{S: &s, Id: HS_ID_CTL}})))
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_ctl_ws{ServerCtl{S: &s, Id: HS_ID_CTL}})))
|
||||||
|
|
||||||
|
// [NOTE]
|
||||||
|
// s.cfg.CtlPrefix applies to "/_ctl/" something only
|
||||||
|
// other endpoins below don't begin with /_ctl. so the prefix doesn't apply.
|
||||||
|
|
||||||
s.ctl_mux.Handle("/_pty/ws",
|
s.ctl_mux.Handle("/_pty/ws",
|
||||||
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pty_ws{S: &s, Id: HS_ID_CTL})))
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pty_ws{S: &s, Id: HS_ID_CTL})))
|
||||||
s.ctl_mux.Handle("/_pty/xterm.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.js"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm-addon-fit.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm-addon-fit.js"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm-addon-fit.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm-addon-unicode11.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm-addon-unicode11.js"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm-addon-unicode11.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm.css",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.css"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm.css/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_pty/xterm.html",
|
s.ctl_mux.Handle("/_pty/xterm.html",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.html", mode: "pty"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.html", mode: "pty"}))
|
||||||
s.ctl_mux.Handle("/_pty/xterm.html/",
|
s.ctl_mux.Handle("/_pty/xterm.html/",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
s.ctl_mux.Handle("/_pty/",
|
s.ctl_mux.Handle("/_pty/{$}",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
||||||
|
|
||||||
s.ctl_mux.Handle("/_rpty/ws",
|
s.ctl_mux.Handle("/_rpty/ws",
|
||||||
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_rpty_ws{S: &s, Id: HS_ID_CTL})))
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_rpty_ws{S: &s, Id: HS_ID_CTL})))
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.js"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm-addon-fit.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm-addon-fit.js"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm-addon-fit.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm-addon-unicode11.js",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm-addon-unicode11.js"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm-addon-unicode11.js/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.css",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.css"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.css/",
|
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.html",
|
s.ctl_mux.Handle("/_rpty/xterm.html",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.html", mode: "rpty"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "xterm.html", mode: "rpty"}))
|
||||||
s.ctl_mux.Handle("/_rpty/xterm.html/",
|
s.ctl_mux.Handle("/_rpty/xterm.html/",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_forbidden"}))
|
||||||
s.ctl_mux.Handle("/_rpty/",
|
s.ctl_mux.Handle("/_rpty/{$}",
|
||||||
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_CTL}, file: "_redir:xterm.html"}))
|
||||||
|
|
||||||
s.ctl_mux.Handle("/_rxc",
|
s.ctl_mux.Handle("/_rxc",
|
||||||
@@ -1703,6 +1710,43 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
s.ect_mux = http.NewServeMux()
|
||||||
|
|
||||||
|
// make some possibly external services accessible on others ports for convenience
|
||||||
|
s.ect_mux.Handle("/_pty/ws",
|
||||||
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pty_ws{S: &s, Id: HS_ID_ECT, Auth: s.Cfg.EctAuth})))
|
||||||
|
s.ect_mux.Handle("/_pty/xterm.html",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "xterm.html", mode: "pty", auth: s.Cfg.EctAuth})) // override the auth field to not use s.Cfg.CtlAuth
|
||||||
|
s.ect_mux.Handle("/_pty/xterm.html/",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "_forbidden"}))
|
||||||
|
s.ect_mux.Handle("/_pty/{$}",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "_redir:xterm.html"}))
|
||||||
|
|
||||||
|
s.ect_mux.Handle("/_rpty/ws",
|
||||||
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_rpty_ws{S: &s, Id: HS_ID_ECT, Auth: s.Cfg.EctAuth})))
|
||||||
|
s.ect_mux.Handle("/_rpty/xterm.html",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "xterm.html", mode: "rpty", auth: s.Cfg.EctAuth})) // override the auth field to not use s.Cfg.CtlAuth
|
||||||
|
s.ect_mux.Handle("/_rpty/xterm.html/",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "_forbidden"}))
|
||||||
|
s.ect_mux.Handle("/_rpty/{$}",
|
||||||
|
s.WrapHttpHandler(&server_pty_xterm_file{ServerCtl: ServerCtl{S: &s, Id: HS_ID_ECT}, file: "_redir:xterm.html"}))
|
||||||
|
|
||||||
|
s.ect = make([]*http.Server, len(cfg.EctAddrs))
|
||||||
|
for i = 0; i < len(cfg.EctAddrs); i++ {
|
||||||
|
s.ect[i] = &http.Server{
|
||||||
|
Addr: cfg.EctAddrs[i],
|
||||||
|
Handler: s.ect_mux,
|
||||||
|
TLSConfig: cfg.EctTls.Clone(),
|
||||||
|
ReadHeaderTimeout: hs_read_header_timeout,
|
||||||
|
IdleTimeout: hs_idle_timeout,
|
||||||
|
MaxHeaderBytes: hs_max_header_bytes,
|
||||||
|
BaseContext: hs_base_ctx,
|
||||||
|
ErrorLog: hs_log_ect,
|
||||||
|
// TODO: more settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
s.rpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
s.rpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
||||||
@@ -1730,34 +1774,17 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_redirect"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_redirect"}))
|
||||||
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/{$}",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_redir:xterm.html"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_redir:xterm.html"}))
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.html",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.html",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm.html"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm.html", HttpAuth: s.Cfg.PxyAuth}))
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.html/",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.html/",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.css",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm.css"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.css/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm.js"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm-addon-fit.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm-addon-fit.js"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm-addon-fit.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm-addon-unicode11.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "xterm-addon-unicode11.js"}))
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/xterm-addon-unicode11.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
|
||||||
|
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/ws",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/ws",
|
||||||
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY})))
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_PXY, HttpAuth: s.Cfg.PxyAuth})))
|
||||||
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/session-info",
|
s.pxy_mux.Handle("/_ssh/{conn_id}/{route_id}/session-info",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_PXY, NoAuth: true}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl: ServerCtl{S: &s, Id: HS_ID_PXY, NoAuth: true}, HttpAuth: s.Cfg.PxyAuth}))
|
||||||
|
|
||||||
s.pxy_mux.Handle("/_ssh/",
|
s.pxy_mux.Handle("/_ssh/",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_PXY}, file: "_forbidden"}))
|
||||||
s.pxy_mux.Handle("/favicon.ico",
|
s.pxy_mux.Handle("/favicon.ico",
|
||||||
@@ -1788,33 +1815,17 @@ func NewServer(ctx context.Context, name string, logger Logger, cfg *ServerConfi
|
|||||||
|
|
||||||
s.wpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
s.wpx_mux = http.NewServeMux() // TODO: make /_init,_ssh,_ssh/ws,_http configurable...
|
||||||
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/",
|
s.wpx_mux.Handle("/_ssh/{port_id}/{$}",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_redir:xterm.html"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_redir:xterm.html"}))
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.html",
|
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.html",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm.html"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm.html", HttpAuth: s.Cfg.WpxAuth}))
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.html/",
|
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.html/",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.css",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm.css"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.css/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm.js"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm-addon-fit.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm-addon-fit.js"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm-addon-fit.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm-addon-unicode11.js",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "xterm-addon-unicode11.js"}))
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/xterm-addon-unicode11.js/",
|
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
|
||||||
|
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/ws",
|
s.wpx_mux.Handle("/_ssh/{port_id}/ws",
|
||||||
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_WPX})))
|
s.SafeWrapWebsocketHandler(s.WrapWebsocketHandler(&server_pxy_ssh_ws{S: &s, Id: HS_ID_WPX, HttpAuth: s.Cfg.WpxAuth })))
|
||||||
s.wpx_mux.Handle("/_ssh/{port_id}/session-info",
|
s.wpx_mux.Handle("/_ssh/{port_id}/session-info",
|
||||||
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl{S: &s, Id: HS_ID_WPX, NoAuth: true}}))
|
s.WrapHttpHandler(&server_ctl_server_conns_id_routes_id{ServerCtl: ServerCtl{S: &s, Id: HS_ID_WPX, NoAuth: true}, HttpAuth: s.Cfg.WpxAuth}))
|
||||||
|
|
||||||
s.wpx_mux.Handle("/_ssh/",
|
s.wpx_mux.Handle("/_ssh/",
|
||||||
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
s.WrapHttpHandler(&server_pxy_xterm_file{server_pxy: server_pxy{S: &s, Id: HS_ID_WPX}, file: "_forbidden"}))
|
||||||
@@ -2008,6 +2019,65 @@ func (s *Server) RunCtlTask(wg *sync.WaitGroup) {
|
|||||||
l_wg.Wait()
|
l_wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s* Server) run_single_ect_server(i int, cs *http.Server, wg* sync.WaitGroup) {
|
||||||
|
var l net.Listener
|
||||||
|
var err error
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
s.log.Write("", LOG_INFO, "Limited external control channel[%d] started on %s", i, cs.Addr)
|
||||||
|
|
||||||
|
if s.stop_req.Load() == false {
|
||||||
|
// defeat hard-coded "tcp" in ListenAndServe() and ListenAndServeTLS()
|
||||||
|
// err = cs.ListenAndServe()
|
||||||
|
// err = cs.ListenAndServeTLS("", "")
|
||||||
|
l, err = net.Listen(TcpAddrStrClass(cs.Addr), cs.Addr)
|
||||||
|
if err == nil {
|
||||||
|
if s.stop_req.Load() == false {
|
||||||
|
var node *list.Element
|
||||||
|
|
||||||
|
s.ect_addrs_mtx.Lock()
|
||||||
|
node = s.ect_addrs.PushBack(l.Addr().(*net.TCPAddr))
|
||||||
|
s.ect_addrs_mtx.Unlock()
|
||||||
|
|
||||||
|
if s.Cfg.EctTls == nil {
|
||||||
|
err = cs.Serve(l)
|
||||||
|
} else {
|
||||||
|
err = cs.ServeTLS(l, "", "") // s.Cfg.EctTls must provide a certificate and a key
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ect_addrs_mtx.Lock()
|
||||||
|
s.ect_addrs.Remove(node)
|
||||||
|
s.ect_addrs_mtx.Unlock()
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("stop requested")
|
||||||
|
}
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("stop requested")
|
||||||
|
}
|
||||||
|
if err == nil || errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.log.Write("", LOG_INFO, "Limited external control channel[%d] ended", i)
|
||||||
|
} else {
|
||||||
|
s.log.Write("", LOG_ERROR, "Limited external control channel[%d] error - %s", i, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) RunEctTask(wg *sync.WaitGroup) {
|
||||||
|
var ect *http.Server
|
||||||
|
var idx int
|
||||||
|
var l_wg sync.WaitGroup
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for idx, ect = range s.ect {
|
||||||
|
l_wg.Add(1)
|
||||||
|
go s.run_single_ect_server(idx, ect, &l_wg);
|
||||||
|
}
|
||||||
|
l_wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) run_single_rpx_server(i int, cs *http.Server, wg* sync.WaitGroup) {
|
func (s *Server) run_single_rpx_server(i int, cs *http.Server, wg* sync.WaitGroup) {
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
var err error
|
var err error
|
||||||
@@ -2195,6 +2265,10 @@ func (s *Server) ReqStop() {
|
|||||||
hs.Shutdown(s.Ctx) // to break s.ctl.Serve()
|
hs.Shutdown(s.Ctx) // to break s.ctl.Serve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, hs = range s.ect {
|
||||||
|
hs.Shutdown(s.Ctx) // to break s.ect.Serve()
|
||||||
|
}
|
||||||
|
|
||||||
for _, hs = range s.rpx {
|
for _, hs = range s.rpx {
|
||||||
hs.Shutdown(s.Ctx) // to break s.rpx.Serve()
|
hs.Shutdown(s.Ctx) // to break s.rpx.Serve()
|
||||||
}
|
}
|
||||||
@@ -2629,6 +2703,11 @@ func (s *Server) StartCtlService() {
|
|||||||
go s.RunCtlTask(&s.wg)
|
go s.RunCtlTask(&s.wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) StartEctService() {
|
||||||
|
s.wg.Add(1)
|
||||||
|
go s.RunEctTask(&s.wg)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) StartRpxService() {
|
func (s *Server) StartRpxService() {
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go s.RunRpxTask(&s.wg)
|
go s.RunRpxTask(&s.wg)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [viteSingleFile()],
|
||||||
|
build: {
|
||||||
|
outDir: ".vite-xterm",
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: resolve(".", "xterm-plain.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skipped minification because the original files appears to be already minified.
|
|
||||||
* Original file: /npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js
|
|
||||||
*
|
|
||||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
|
||||||
*/
|
|
||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
|
||||||
//# sourceMappingURL=xterm-addon-fit.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,128 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Terminal</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-info-container {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-target {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-status {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-errmsg {
|
||||||
|
display: inline-block;
|
||||||
|
color: red;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-control {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-control button {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#terminal-view-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
/*height: 100%;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
#login-form-container {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#login-form-title {
|
||||||
|
line-height: 2em;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module" src="xterm-plain.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-xt-mode="{{@@ .Mode @@}}" data-conn-id="{{@@ .ConnId @@}}" data-route-id="{{@@ .RouteId @@}}">
|
||||||
|
|
||||||
|
<div id="login-container">
|
||||||
|
<div id="login-form-container">
|
||||||
|
<div id="login-form-title"></div>
|
||||||
|
<form id="login-form">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div id="terminal-container">
|
||||||
|
<div id="terminal-info-container">
|
||||||
|
<div id="terminal-target"></div>
|
||||||
|
<div id="terminal-status"></div>
|
||||||
|
<div id="terminal-errmsg"></div>
|
||||||
|
<div id="terminal-control">
|
||||||
|
<button id="terminal-disconnect" type="button">Disconnect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="terminal-view-container"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
import '@xterm/xterm/css/xterm.css';
|
||||||
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
|
|
||||||
|
function utf8ToBase64(str) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(str);
|
||||||
|
const binaryString = String.fromCharCode.apply(null, data);
|
||||||
|
return btoa(binaryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToBinary(b64) {
|
||||||
|
const binaryString = atob(b64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
let i;
|
||||||
|
|
||||||
|
for (i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function(event) {
|
||||||
|
const xt_mode = document.body.dataset.xtMode || "";
|
||||||
|
const conn_id = document.body.dataset.connId || "";
|
||||||
|
const route_id = document.body.dataset.routeId || "";
|
||||||
|
const terminal_target = document.getElementById("terminal-target");
|
||||||
|
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);
|
||||||
|
const term = new Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
fontFamily: "Ubuntu Mono, Consolas, SF Mono, courier-new, courier, monospace",
|
||||||
|
allowProposedApi: true
|
||||||
|
});
|
||||||
|
const fit_addon = new FitAddon();
|
||||||
|
const unicode11_addon = new Unicode11Addon();
|
||||||
|
const text_decoder = new TextDecoder();
|
||||||
|
|
||||||
|
void event;
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
term.loadAddon(fit_addon);
|
||||||
|
term.loadAddon(unicode11_addon);
|
||||||
|
term.unicode.activeVersion = "11";
|
||||||
|
term.open(terminal_view_container);
|
||||||
|
|
||||||
|
const set_terminal_target = function(name) {
|
||||||
|
terminal_target.innerText = name;
|
||||||
|
login_form_title.innerText = name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const set_terminal_status = function(msg, errmsg) {
|
||||||
|
if (msg != null) terminal_status.innerText = msg;
|
||||||
|
if (errmsg != null) {
|
||||||
|
if (errmsg != "") {
|
||||||
|
const d = new Date();
|
||||||
|
terminal_errmsg.innerText = "[" + d.toLocaleString() + "] " + errmsg;
|
||||||
|
} else {
|
||||||
|
terminal_errmsg.innerText = errmsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjust_terminal_size_unconnected = function() {
|
||||||
|
fit_addon.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetch_session_info = async function() {
|
||||||
|
let url = window.location.protocol + "//" + window.location.host;
|
||||||
|
let pathname = window.location.pathname;
|
||||||
|
|
||||||
|
const qparams = new URLSearchParams(window.location.search);
|
||||||
|
const xparams = new URLSearchParams();
|
||||||
|
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf("/"));
|
||||||
|
url += pathname + "/session-info";
|
||||||
|
|
||||||
|
const access_token = qparams.get("access-token");
|
||||||
|
if (access_token !== null && access_token != "") xparams.set("access-token", access_token);
|
||||||
|
if (xparams.size > 0) url += "?" + xparams.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
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"]);
|
||||||
|
document.title = route["client-peer-name"];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
set_terminal_target("");
|
||||||
|
document.title = "";
|
||||||
|
set_terminal_status(null, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle_login_form = function(visible) {
|
||||||
|
if (visible && xt_mode == "ssh") fetch_session_info();
|
||||||
|
login_container.style.visibility = (visible ? "visible" : "hidden");
|
||||||
|
terminal_disconnect.style.visibility = (visible ? "hidden" : "visible");
|
||||||
|
if (visible) {
|
||||||
|
if (xt_mode == "ssh") username_field.focus();
|
||||||
|
else terminal_connect.focus();
|
||||||
|
} else {
|
||||||
|
term.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle_login_form(true);
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
adjust_terminal_size_unconnected();
|
||||||
|
|
||||||
|
login_form.onsubmit = async function(event) {
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
const prefix = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
let pathname = window.location.pathname;
|
||||||
|
const xparams = new URLSearchParams();
|
||||||
|
let url;
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
toggle_login_form(false);
|
||||||
|
|
||||||
|
if (xt_mode == "ssh") {
|
||||||
|
username = username_field.value.trim();
|
||||||
|
password = password_field.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
pathname = pathname.substring(0, pathname.lastIndexOf("/"));
|
||||||
|
url = prefix + window.location.host + pathname + "/ws";
|
||||||
|
|
||||||
|
const access_token = qparams.get("access-token");
|
||||||
|
if (access_token !== null && access_token != "") xparams.set("access-token", access_token);
|
||||||
|
if (xt_mode == "rpty") {
|
||||||
|
const client_token = qparams.get("client-token");
|
||||||
|
if (client_token !== null && client_token != "") xparams.set("client-token", client_token);
|
||||||
|
}
|
||||||
|
if (xparams.size > 0) url += "?" + xparams.toString();
|
||||||
|
|
||||||
|
socket = new WebSocket(url);
|
||||||
|
socket.binaryType = "arraybuffer";
|
||||||
|
|
||||||
|
set_terminal_status("Connecting...", "");
|
||||||
|
|
||||||
|
const adjust_terminal_size_connected = function() {
|
||||||
|
fit_addon.fit();
|
||||||
|
if (socket.readyState == 1)
|
||||||
|
socket.send(JSON.stringify({ type: "size", data: [term.rows.toString(), term.cols.toString()] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onopen = function() {
|
||||||
|
socket.send(JSON.stringify({ type: "open", data: [username, password] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
let event_text;
|
||||||
|
let parsed_msg;
|
||||||
|
|
||||||
|
event_text = (typeof event.data === "string") ? event.data : text_decoder.decode(new Uint8Array(event.data));
|
||||||
|
parsed_msg = JSON.parse(event_text);
|
||||||
|
if (parsed_msg.type == "iov") {
|
||||||
|
for (const data of parsed_msg.data) term.write(base64ToBinary(data));
|
||||||
|
} else if (parsed_msg.type == "status") {
|
||||||
|
if (parsed_msg.data.length >= 1) {
|
||||||
|
if (parsed_msg.data[0] == "opened") {
|
||||||
|
set_terminal_status("Connected", "");
|
||||||
|
adjust_terminal_size_connected();
|
||||||
|
term.clear();
|
||||||
|
} else if (parsed_msg.data[0] == "closed") {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parsed_msg.type == "error") {
|
||||||
|
set_terminal_status(null, parsed_msg.data.join(" "));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
set_terminal_status(null, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(event) {
|
||||||
|
void event;
|
||||||
|
set_terminal_status("Disconnected", "");
|
||||||
|
toggle_login_form(true);
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function() {
|
||||||
|
set_terminal_status("Disconnected", null);
|
||||||
|
toggle_login_form(true);
|
||||||
|
window.onresize = adjust_terminal_size_unconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
term.onData(function(data) {
|
||||||
|
if (socket.readyState == 1)
|
||||||
|
socket.send(JSON.stringify({ type: "iov", data: [utf8ToBase64(data)] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onresize = adjust_terminal_size_connected;
|
||||||
|
terminal_disconnect.onclick = function(event) {
|
||||||
|
void event;
|
||||||
|
socket.send(JSON.stringify({ type: "close", data: [""] }));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Minified by jsDelivr using clean-css v5.3.2.
|
|
||||||
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
|
||||||
*
|
|
||||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
|
||||||
*/
|
|
||||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
|
||||||
+41
-227
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user