package main import ( "context" "fmt" "git.wamblee.org/converge/pkg/server/matchmaker" "git.wamblee.org/converge/pkg/support/websocketutil" "log" "net" "net/http" _ "net/http/pprof" "os" "regexp" "strings" _ "time/tzdata" ) func parsePublicId(path string) (publicId string, _ error) { pattern := regexp.MustCompile("/([^/]+)$") matches := pattern.FindStringSubmatch(path) if len(matches) != 2 { return "", fmt.Errorf("Invalid URL path '%s'", path) } return matches[1], nil } func catchAllHandler(contextPath string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, contextPath+"docs", http.StatusFound) } } func printHelp(msg string) { if msg != "" { fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", msg) } helpText := "Usage: converge [options]\n" + "\n" + "Converge server is a rendez-vous server for debugging continuous integration\n" + "jobs be providing the capability to log into the agents where jobs are running.\n" + "This is achieve by starting an agent in the continuous integration job\n" + "which connects to Converge using a websocket connection. The end user also connects\n" + "to Converge using ssh over websockets. The server then matches the end-user with\n" + "the agent running in the continous integration job (the rendez-vous) and sets up\n" + "an end-to-end SSH connection between end-user and agent, with the agent providing\n" + "an embedded SSH server to provide interactive access to the end-user. This works\n" + "both on linux and on windows.\n" + "\n" + "Options\n" + "--static : directory where static content of converge is placed\n" + "--downloads : directory where downloads of converge are placed\n" + "--ccntext : by default all content is served at /. Use this option to specify\n" + " a different context path. For instance to host converge at a base\n" + " URL of https://example.com/converge/, specify /converge/ (with\n" + " trailing slash. \n" + "--pprof: Enable the pprof endpoint at /debug/pprof" fmt.Fprintln(os.Stderr, helpText) os.Exit(1) } func main() { // for debugging hangs relating to using unbuffered channels //go func() { // buf := make([]byte, 1000000) // n := runtime.Stack(buf, true) // time.Sleep(10 * time.Second) // log.Println("LOG: \n" + string(buf[:n])) //}() downloaddir := "." staticdir := "../static" contextpath := "/" pprof := false args := os.Args[1:] for len(args) > 0 && strings.HasPrefix(args[0], "-") { switch args[0] { case "--downloads": if len(args) <= 1 { printHelp("The --downloads option expects an argument") } downloaddir = args[1] args = args[1:] case "--static": if len(args) <= 1 { printHelp("The --static option expects an argument") } staticdir = args[1] args = args[1:] case "--context": if len(args) <= 1 { printHelp("The --context option expects an argument") } contextpath = args[1] args = args[1:] case "--pprof": pprof = true default: printHelp("Unknown option " + args[0]) } args = args[1:] } log.Println("Content directory", staticdir) if len(args) != 0 { printHelp("") } // Initializing the ocre system. // Setup of websession handling for pushing notifications to the client // Prometheus // And the MatchMaker. The MatchMakers sends state notifications to websessions // and prometheus. notifications := NewStateNotifier() websessions := matchmaker.NewWebSessions(notifications.webNotificationChannel) // monitoring prometheusMux := http.NewServeMux() setupPrometheus(prometheusMux, notifications.prometheusNotificationChannel) go func() { log.Fatal(http.ListenAndServe(":8001", prometheusMux)) }() admin := matchmaker.NewMatchMaker(notifications) registrationService, clientService, sessionService := setupWebSockets(admin, websessions) context := HttpContext{mux: http.NewServeMux(), path: contextpath} if pprof { registerPprof(context.mux, "/debug/pprof") } setupWebUI(context, registrationService, clientService, sessionService, staticdir, downloaddir) // Catch all for the web UI context.mux.HandleFunc("/", catchAllHandler(contextpath)) // Start HTTP server fmt.Println("Rendez-vous server listening on :8000") log.Fatal(http.ListenAndServe(":8000", context.mux)) } func setupWebUI(context HttpContext, registrationService websocketutil.WebSocketService, clientService websocketutil.WebSocketService, sessionService websocketutil.WebSocketService, staticdir string, downloaddir string) HttpContext { // The web UI context.HandleFunc("agent/", registrationService.Handle) context.HandleFunc("client/", clientService.Handle) context.HandleFunc("ws/sessions", sessionService.Handle) // create filehandler with templating for html files. context.Handle("docs/", http.StripPrefix("docs/", http.HandlerFunc(pageHandler))) context.Handle("static/", http.StripPrefix("static/", http.FileServer(http.Dir(staticdir)))) context.Handle("downloads/", http.StripPrefix("downloads/", http.FileServer(http.Dir(downloaddir)))) // create usage generator context.HandleFunc("usage", generateCLIExammple) return context } func setupWebSockets(admin *matchmaker.MatchMaker, websessions *matchmaker.WebSessions) (websocketutil.WebSocketService, websocketutil.WebSocketService, websocketutil.WebSocketService) { // websocket endpoints // For agents connecting registrationService := websocketutil.WebSocketService{ Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) { publicId, err := parsePublicId(r.URL.Path) if err != nil { log.Printf("Cannot parse public id from url: '%v'\n", err) return } log.Printf("Got registration connection: '%s'\n", publicId) err = admin.Register(publicId, conn) if err != nil { log.Printf("Error %v\n", err) } }, } // For users connecting with ssh clientService := websocketutil.WebSocketService{ Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) { publicId, err := parsePublicId(r.URL.Path) if err != nil { log.Printf("Cannot parse public id from url: '%v'\n", err) return } _, wsProxyMode := r.URL.Query()["wsproxy"] log.Printf("Got client connection: '%s'\n", publicId) err = admin.Connect(wsProxyMode, publicId, conn) if err != nil { log.Printf("Error %v\n", err) } }, } // for the web browser getting live status updates. sessionService := websocketutil.WebSocketService{ Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) { ctx, cancel := context.WithCancel(context.Background()) websession := websessions.NewSession(conn, ctx, cancel) defer websessions.SessionClosed(websession) location, err := matchmaker.GetUserLocation(r) if err != nil { panic(err) } websession.WriteNotifications(location, ctx, cancel) }, Text: true, } return registrationService, clientService, sessionService }