diff --git a/server-proxy.go b/server-proxy.go new file mode 100644 index 0000000..220c988 --- /dev/null +++ b/server-proxy.go @@ -0,0 +1,589 @@ +package hodu + +import "context" +import "crypto/tls" +import _ "embed" +import "encoding/json" +import "fmt" +import "io" +import "net" +import "net/http" +import "net/url" +import "strconv" +import "strings" +import "text/template" +import "unsafe" + +import "golang.org/x/crypto/ssh" +import "golang.org/x/net/http/httpguts" +import "golang.org/x/net/websocket" + +const SERVER_PROXY_ID_COOKIE string = "hodu-proxy-id" +const SERVER_PROXY_MODE_COOKIE string = "hodu-proxy-mode" + +const SERVER_PROXY_MODE_VERBATIM string = "verbatim" +const SERVER_PROXY_MODE_PREFIXED string = "prefixed" + +//go:embed xterm.js +var xterm_js []byte +//go:embed xterm-addon-fit.js +var xterm_addon_fit_js []byte +//go:embed xterm.css +var xterm_css []byte +//go:embed xterm.html +var xterm_html []byte + +type server_proxy_http_init struct { + s *Server +} + +type server_proxy_http_main struct { + s *Server +} + +type server_proxy_ssh struct { + s *Server +} +// ------------------------------------ + +//Copied from net/http/httputil/reverseproxy.go +var hopHeaders = []string{ + "Connection", + "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailers", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 + "Transfer-Encoding", + "Upgrade", +} + +func copy_headers(src http.Header, dst http.Header) { + var key string + var val string + var vals []string + + for key, vals = range src { + for _, val = range vals { + dst.Add(key, val) + } + } +} + +func delete_hop_by_hop_headers(header http.Header) { + var h string + + for _, h = range hopHeaders { + header.Del(h) + } +} + +func mutate_proxy_req_headers(req *http.Request, newreq *http.Request) { + var hdr http.Header + var newhdr http.Header + var remote_addr string + var local_port string + var oldv []string + var ok bool + var err error + var conn_addr net.Addr + + //newreq.Header = req.Header.Clone() + copy_headers(req.Header, newreq.Header) + delete_hop_by_hop_headers(newreq.Header) + + hdr = req.Header + newhdr = newreq.Header + remote_addr, _, err = net.SplitHostPort(req.RemoteAddr) + if err == nil { + oldv, ok = hdr["X-Forwarded-For"] + if ok { remote_addr = strings.Join(oldv, ", ") + ", " + remote_addr } + newhdr.Set("X-Forwarded-For", remote_addr) + } + + conn_addr, ok = req.Context().Value(http.LocalAddrContextKey).(net.Addr) + if ok { + _, local_port, err = net.SplitHostPort(conn_addr.String()) + if err == nil { + oldv, ok = newhdr["X-Forwarded-Port"] + if !ok { newhdr.Set("X-Fowarded-Port", local_port) } + } + } + + _, ok = newhdr["X-Forwarded-Proto"] + if !ok { + var proto string + if req.TLS == nil { + proto = "http" + } else { + proto = "https" + } + newhdr.Set("X-Fowarded-Proto", proto) + } + + _, ok = newhdr["X-Forwarded-Host"] + if !ok { + newhdr.Set("X-Forwarded-Host", req.Host) + } +} +// ------------------------------------ + +func (pxy *server_proxy_http_init) ServeHTTP(w http.ResponseWriter, req *http.Request) { + var s *Server + var r *ServerRoute + var status_code int + var conn_id string + var conn_nid uint64 + var route_id string + var route_nid uint64 + var err error + + defer func() { + var err interface{} = recover() + if err != nil { dump_call_frame_and_exit(pxy.s.log, req, err) } + }() + + s = pxy.s + + conn_id = req.PathValue("conn_id") + route_id = req.PathValue("route_id") + + conn_nid, err = strconv.ParseUint(conn_id, 10, int(unsafe.Sizeof(conn_nid) * 8)) + if err != nil { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + goto oops + } + route_nid, err = strconv.ParseUint(route_id, 10, int(unsafe.Sizeof(route_nid) * 8)) + if err != nil { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + goto oops + } + + r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid)) + if r == nil { + status_code = http.StatusNotFound; w.WriteHeader(status_code) + goto done + } + + w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%s; Path=/; HttpOnly", SERVER_PROXY_MODE_COOKIE, SERVER_PROXY_MODE_VERBATIM)) + w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%d-%d; Path=/; HttpOnly", SERVER_PROXY_ID_COOKIE, conn_nid, route_nid)) // use the interpreted ids. + w.Header().Set("Location", strings.TrimPrefix(req.URL.Path, fmt.Sprintf("/_init/%s/%s", conn_id, route_id))) // use the orignal id srings + status_code = http.StatusFound; w.WriteHeader(status_code) + +done: + s.log.Write("", LOG_INFO, "[%s] %s %s %d", req.RemoteAddr, req.Method, req.URL.String(), status_code) + return + +oops: + s.log.Write("", LOG_ERROR, "[%s] %s %s %d - %s", req.RemoteAddr, req.Method, req.URL.String(), status_code, err.Error()) + return +} + +// ------------------------------------ + +func prevent_follow_redirect (req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse +} + +func (pxy *server_proxy_http_main) ServeHTTP(w http.ResponseWriter, req *http.Request) { + var s *Server + var r *ServerRoute + var status_code int + var mode *http.Cookie + var id *http.Cookie + var ids []string + var conn_nid uint64 + var route_nid uint64 + var client *http.Client + var resp *http.Response + var tcp_conn *net.TCPConn + var transport *http.Transport + var addr net.TCPAddr + var proxy_req *http.Request + var proxy_url *url.URL + var proxy_proto string + var req_upgrade_type string + var err error + + defer func() { + var err interface{} = recover() + if err != nil { dump_call_frame_and_exit(pxy.s.log, req, err) } + }() + + s = pxy.s + +/* + ctx := req.Context() + if ctx.Done() != nil { + } +*/ + mode, err = req.Cookie(SERVER_PROXY_MODE_COOKIE) + if err == nil { + if (mode.Value == SERVER_PROXY_MODE_PREFIXED) { + // TODO: + } + } + + id, err = req.Cookie(SERVER_PROXY_ID_COOKIE) + if err != nil { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + goto oops + } + + ids = strings.Split(id.Value, "-") + if (len(ids) != 2) { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + err = fmt.Errorf("invalid proxy id cookie value - %s", id.Value) + goto oops + } + + conn_nid, err = strconv.ParseUint(ids[0], 10, int(unsafe.Sizeof(conn_nid) * 8)) + if err != nil { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + goto oops + } + route_nid, err = strconv.ParseUint(ids[1], 10, int(unsafe.Sizeof(route_nid) * 8)) + if err != nil { + status_code = http.StatusBadRequest; w.WriteHeader(status_code) + goto oops + } + + r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid)) + if r == nil { + status_code = http.StatusNotFound; w.WriteHeader(status_code) + goto done + } + + addr = *r.svc_addr; + if addr.IP.To4() != nil { + addr.IP = net.IPv4(127, 0, 0, 1) // net.IPv4loopback is not defined. so use net.IPv4() + } else { + addr.IP = net.IPv6loopback // net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + } + tcp_conn, err = net.DialTCP("tcp", nil, &addr) // need to be specific between tcp4 and tcp6? maybe not + if err != nil { + status_code = http.StatusBadGateway; w.WriteHeader(status_code) + goto oops + } + + transport = &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return tcp_conn, nil + }, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client = &http.Client{ + Transport: transport, + CheckRedirect: prevent_follow_redirect, + } + + // HTTP or HTTPS is actually a hint to the client-side peer + // Use the hint to compose the URL to the client via the server-side + // listening socket as if it connects to the client-side peer + if r.svc_option & RouteOption(ROUTE_OPTION_HTTPS) != 0 { + proxy_proto = "https" + } else { + proxy_proto = "http" + } + + //proxy_url = fmt.Sprintf("%s://%s%s", proxy_proto, r.ptc_addr, req.URL.Path) + proxy_url = &url.URL{ + Scheme: proxy_proto, + Host: r.ptc_addr, + Path: req.URL.Path, + RawQuery: req.URL.RawQuery, + Fragment: req.URL.Fragment, + } + +// TODO: http.NewRequestWithContext().?? + proxy_req, err = http.NewRequest(req.Method, proxy_url.String(), req.Body) + if err != nil { + status_code = http.StatusInternalServerError; w.WriteHeader(status_code) + goto oops + } + + if httpguts.HeaderValuesContainsToken(req.Header["Connection"], "Upgrade") { + req_upgrade_type = req.Header.Get("Upgrade") + } + mutate_proxy_req_headers(req, proxy_req) + + if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") { + proxy_req.Header.Set("Te", "trailers") + } + if req_upgrade_type != "" { + proxy_req.Header.Set("Connection", "Upgrade") + proxy_req.Header.Set("Upgrade", req_upgrade_type) + } + +//fmt.Printf ("proxy NEW req [%+v]\n", proxy_req) + + resp, err = client.Do(proxy_req) + if err != nil { + status_code = http.StatusInternalServerError; w.WriteHeader(status_code) + goto oops + } else { + var hdr http.Header + //var loc string + + defer resp.Body.Close() + + if resp.StatusCode == http.StatusSwitchingProtocols { + // TODO: + } + + hdr = w.Header() + copy_headers(resp.Header, hdr) + delete_hop_by_hop_headers(hdr) + /* + loc = hdr.Get("Location") + if loc != "" { + strings.Replace(lv, r.ptc_addr, req.Host + hdr.Set("Location", xxx) + }*/ + + w.Header().Add("Set-Cookie", fmt.Sprintf("%s=%d-%d; Path=/; HttpOnly", SERVER_PROXY_ID_COOKIE, conn_nid, route_nid)) + status_code = resp.StatusCode; w.WriteHeader(status_code) + io.Copy(w, resp.Body) + + // TODO: handle trailers + } + +done: + s.log.Write("", LOG_INFO, "[%s] %s %s %d", req.RemoteAddr, req.Method, req.URL.String(), status_code) + return + +oops: + s.log.Write("", LOG_ERROR, "[%s] %s %s %d - %s", req.RemoteAddr, req.Method, req.URL.String(), status_code, err.Error()) + return +} + + +// ------------------------------------ +type server_proxy_xterm_file struct { + s *Server + file string +} + +type server_proxy_xterm_session_info struct { + ConnId string + RouteId string +} + +func (pxy *server_proxy_xterm_file) ServeHTTP(w http.ResponseWriter, req *http.Request) { + defer func() { + var err interface{} = recover() + if err != nil { dump_call_frame_and_exit(pxy.s.log, req, err) } + }() + +// TODO: logging + switch pxy.file { + case "xterm.js": + w.Header().Set("Content-Type", "text/javascript") + w.WriteHeader(http.StatusOK) + w.Write(xterm_js) + case "xterm-addon-fit.js": + w.Header().Set("Content-Type", "text/javascript") + w.WriteHeader(http.StatusOK) + w.Write(xterm_addon_fit_js) + case "xterm.css": + w.Header().Set("Content-Type", "text/css") + w.WriteHeader(http.StatusOK) + w.Write(xterm_css) + case "xterm.html": + var tmpl *template.Template + var err error + + tmpl = template.New("") + _, err = tmpl.Parse(string(xterm_html)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + tmpl.Execute(w, + &server_proxy_xterm_session_info{ + req.PathValue("conn_id"), + req.PathValue("route_id"), + }) + } + case "_forbidden": + w.WriteHeader(http.StatusForbidden) + default: + w.WriteHeader(http.StatusNotFound) + } +} +// ------------------------------------ + +type server_proxy_ssh_ws struct { + s *Server + h websocket.Handler +} + +type json_ssh_ws_event struct { + Type string `json:"type"` + Data string `json:"data"` +} + +// TODO: put this task to sync group. +// TODO: put the above proxy task to sync group too. + + +func server_proxy_serve_ssh_ws(ws *websocket.Conn, s *Server) { + var req *http.Request + var conn_id string + var conn_nid uint64 + var route_id string + var route_nid uint64 + var r *ServerRoute + var addr net.TCPAddr + var cc *ssh.ClientConfig + var c *ssh.Client + var sess *ssh.Session + var in io.Writer + var out io.Reader + var err error + + req = ws.Request() + + defer func() { + var err interface{} = recover() + if err != nil { dump_call_frame_and_exit(s.log, req, err) } + }() + + conn_id = req.PathValue("conn_id") + route_id = req.PathValue("route_id") + + conn_nid, err = strconv.ParseUint(conn_id, 10, int(unsafe.Sizeof(conn_nid) * 8)) + if err != nil { + return + } + route_nid, err = strconv.ParseUint(route_id, 10, int(unsafe.Sizeof(route_nid) * 8)) + if err != nil { + return + } + + r = s.FindServerRouteById(ConnId(conn_nid), RouteId(route_nid)) + if r == nil { + // TODO: enhance logging. original request, conn_nid, route_nid + s.log.Write("", LOG_ERROR, "No server route(%d,%d) found", conn_nid, route_nid) + return + } + + cc = &ssh.ClientConfig{ + User: "hyung-hwan", + Auth: []ssh.AuthMethod{ + ssh.Password("evianilie99"), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + +// CHECK OPTIONS + // if r.svc_option & RouteOption(ROUTE_OPTION_SSH) == 0 { + // REJECT?? + //} +// TODO: timeout... + addr = *r.svc_addr; + if addr.IP.To4() != nil { + addr.IP = net.IPv4(127, 0, 0, 1) // net.IPv4loopback is not defined. so use net.IPv4() + } else { + addr.IP = net.IPv6loopback // net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + } + c, err = ssh.Dial("tcp", addr.String(), cc) + if err != nil { + s.log.Write("", LOG_ERROR, "SSH dial error - %s", err.Error()) + return + } + defer c.Close() + + sess, err = c.NewSession() + if err != nil { + s.log.Write("", LOG_ERROR, "SSH session error - %s", err.Error()) + return + } + defer sess.Close() + + out, err = sess.StdoutPipe() + if err != nil { + s.log.Write("", LOG_ERROR, "STDOUT pipe error - ", err.Error()) + return + } + + in, err = sess.StdinPipe() + if err != nil { + s.log.Write("", LOG_ERROR, "STDIN pipe error - ", err.Error()) + return + } + + err = sess.RequestPty("xterm", 40, 80, ssh.TerminalModes{}) + if err != nil { + s.log.Write("", LOG_ERROR, "Request PTY error - ", err.Error()) + return + } + + err = sess.Shell() + if err != nil { + s.log.Write("", LOG_ERROR, "Start shell error - ", err.Error()) + return + } + + // Async reader and writer to websocket + go func() { + var buf []byte + var n int + var err error + + defer sess.Close() + buf = make([]byte, 1024) + for { + n, err = out.Read(buf) + if err != nil { + if err != io.EOF { + s.log.Write("", LOG_ERROR, "Read from SSH stdout error:", err) + } + return + } + if n > 0 { + _, err = ws.Write(buf[:n]) + if err != nil { + s.log.Write("", LOG_ERROR, "Write to WebSocket error:", err) + return + } + } + } + }() + + // Sync websocket reader and writer to sshIn + for { + var msg []byte + err = websocket.Message.Receive(ws, &msg) + if err != nil { + // TODO: check if EOF + s.log.Write("", LOG_ERROR, "Failed to read from websocket - %s", err.Error()) + break + } + if len(msg) > 0 { + var ev json_ssh_ws_event + err = json.Unmarshal(msg, &ev) + if err == nil { + switch ev.Type { + case "key": + in.Write([]byte(ev.Data)) + case "resize": + var sz []string + sz = strings.Fields(ev.Data) + if (len(sz) == 2) { + var rows int + var cols int + rows, _ = strconv.Atoi(sz[0]); + cols, _ = strconv.Atoi(sz[1]); + sess.WindowChange(rows, cols) + s.log.Write("", LOG_DEBUG, "Resized terminal to %d,%d", rows, cols) + // ignore error + } + } + } + } + } +}