go-micro.dev/v5@v5.12.0/cmd/micro/server/server.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "crypto/rsa" 7 "crypto/x509" 8 "encoding/base64" 9 "encoding/json" 10 "encoding/pem" 11 "fmt" 12 "io" 13 "io/fs" 14 "log" 15 "net/http" 16 "os" 17 "path/filepath" 18 "sort" 19 "strconv" 20 "strings" 21 "sync" 22 "syscall" 23 "text/template" 24 "time" 25 26 "github.com/urfave/cli/v2" 27 "go-micro.dev/v5/cmd" 28 "go-micro.dev/v5/registry" 29 "go-micro.dev/v5/store" 30 "golang.org/x/crypto/bcrypt" 31 ) 32 33 // HTML is the embedded filesystem for templates and static files, set by main.go 34 var HTML fs.FS 35 36 var ( 37 apiCache struct { 38 sync.Mutex 39 data map[string]any 40 time time.Time 41 } 42 ) 43 44 type templates struct { 45 api *template.Template 46 service *template.Template 47 form *template.Template 48 home *template.Template 49 logs *template.Template 50 log *template.Template 51 status *template.Template 52 authTokens *template.Template 53 authLogin *template.Template 54 authUsers *template.Template 55 } 56 57 type TemplateUser struct { 58 ID string 59 } 60 61 // Define a local Account struct to replace auth.Account 62 // (matches fields used in the code) 63 type Account struct { 64 ID string `json:"id"` 65 Type string `json:"type"` 66 Scopes []string `json:"scopes"` 67 Metadata map[string]string `json:"metadata"` 68 } 69 70 func parseTemplates() *templates { 71 return &templates{ 72 api: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/api.html")), 73 service: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/service.html")), 74 form: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/form.html")), 75 home: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/home.html")), 76 logs: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/logs.html")), 77 log: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/log.html")), 78 status: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/status.html")), 79 authTokens: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_tokens.html")), 80 authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")), 81 authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")), 82 } 83 } 84 85 // Helper to render templates 86 func render(w http.ResponseWriter, tmpl *template.Template, data any) error { 87 return tmpl.Execute(w, data) 88 } 89 90 // Helper to extract user info from JWT cookie 91 func getUser(r *http.Request) string { 92 cookie, err := r.Cookie("micro_token") 93 if err != nil || cookie.Value == "" { 94 return "" 95 } 96 // Parse JWT claims (just decode, don't verify) 97 parts := strings.Split(cookie.Value, ".") 98 if len(parts) != 3 { 99 return "" 100 } 101 payload, err := decodeSegment(parts[1]) 102 if err != nil { 103 return "" 104 } 105 var claims map[string]any 106 if err := json.Unmarshal(payload, &claims); err != nil { 107 return "" 108 } 109 if sub, ok := claims["sub"].(string); ok { 110 return sub 111 } 112 if id, ok := claims["id"].(string); ok { 113 return id 114 } 115 return "" 116 } 117 118 // Helper to decode JWT base64url segment 119 func decodeSegment(seg string) ([]byte, error) { 120 // JWT uses base64url, no padding 121 missing := len(seg) % 4 122 if missing != 0 { 123 seg += strings.Repeat("=", 4-missing) 124 } 125 return decodeBase64Url(seg) 126 } 127 128 func decodeBase64Url(s string) ([]byte, error) { 129 return base64.URLEncoding.DecodeString(s) 130 } 131 132 // Helper: store JWT token 133 func storeJWTToken(storeInst store.Store, token, userID string) { 134 storeInst.Write(&store.Record{Key: "jwt/" + token, Value: []byte(userID)}) 135 } 136 137 // Helper: check if JWT token is revoked (not present in store) 138 func isTokenRevoked(storeInst store.Store, token string) bool { 139 recs, _ := storeInst.Read("jwt/" + token) 140 return len(recs) == 0 141 } 142 143 // Helper: delete all JWT tokens for a user 144 func deleteUserTokens(storeInst store.Store, userID string) { 145 recs, _ := storeInst.Read("jwt/", store.ReadPrefix()) 146 for _, rec := range recs { 147 if string(rec.Value) == userID { 148 storeInst.Delete(rec.Key) 149 } 150 } 151 } 152 153 // Updated authRequired to accept storeInst as argument 154 func authRequired(storeInst store.Store) func(http.HandlerFunc) http.HandlerFunc { 155 return func(next http.HandlerFunc) http.HandlerFunc { 156 return func(w http.ResponseWriter, r *http.Request) { 157 var token string 158 // 1. Check Authorization: Bearer header 159 authz := r.Header.Get("Authorization") 160 if strings.HasPrefix(authz, "Bearer ") { 161 token = strings.TrimPrefix(authz, "Bearer ") 162 token = strings.TrimSpace(token) 163 } 164 // 2. Fallback to micro_token cookie if no header 165 if token == "" { 166 cookie, err := r.Cookie("micro_token") 167 if err == nil && cookie.Value != "" { 168 token = cookie.Value 169 } 170 } 171 if token == "" { 172 if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { 173 w.Header().Set("Content-Type", "application/json") 174 w.WriteHeader(http.StatusUnauthorized) 175 w.Write([]byte(`{"error":"missing or invalid token"}`)) 176 return 177 } 178 // For API endpoints, return 401. For UI, redirect to login. 179 if strings.HasPrefix(r.URL.Path, "/api/") { 180 w.WriteHeader(http.StatusUnauthorized) 181 w.Write([]byte("Unauthorized: missing token")) 182 return 183 } 184 http.Redirect(w, r, "/auth/login", http.StatusFound) 185 return 186 } 187 claims, err := ParseJWT(token) 188 if err != nil { 189 if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { 190 w.Header().Set("Content-Type", "application/json") 191 w.WriteHeader(http.StatusUnauthorized) 192 w.Write([]byte(`{"error":"invalid token"}`)) 193 return 194 } 195 if strings.HasPrefix(r.URL.Path, "/api/") { 196 w.WriteHeader(http.StatusUnauthorized) 197 w.Write([]byte("Unauthorized: invalid token")) 198 return 199 } 200 http.Redirect(w, r, "/auth/login", http.StatusFound) 201 return 202 } 203 if exp, ok := claims["exp"].(float64); ok { 204 if int64(exp) < time.Now().Unix() { 205 if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { 206 w.Header().Set("Content-Type", "application/json") 207 w.WriteHeader(http.StatusUnauthorized) 208 w.Write([]byte(`{"error":"token expired"}`)) 209 return 210 } 211 if strings.HasPrefix(r.URL.Path, "/api/") { 212 w.WriteHeader(http.StatusUnauthorized) 213 w.Write([]byte("Unauthorized: token expired")) 214 return 215 } 216 http.Redirect(w, r, "/auth/login", http.StatusFound) 217 return 218 } 219 } 220 // Check for token revocation 221 if isTokenRevoked(storeInst, token) { 222 if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" { 223 w.Header().Set("Content-Type", "application/json") 224 w.WriteHeader(http.StatusUnauthorized) 225 w.Write([]byte(`{"error":"token revoked"}`)) 226 return 227 } 228 if strings.HasPrefix(r.URL.Path, "/api/") { 229 w.WriteHeader(http.StatusUnauthorized) 230 w.Write([]byte("Unauthorized: token revoked")) 231 return 232 } 233 http.Redirect(w, r, "/auth/login", http.StatusFound) 234 return 235 } 236 next(w, r) 237 } 238 } 239 } 240 241 func wrapAuth(authRequired func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc { 242 return func(h http.HandlerFunc) http.HandlerFunc { 243 return func(w http.ResponseWriter, r *http.Request) { 244 path := r.URL.Path 245 if strings.HasPrefix(path, "/auth/login") || strings.HasPrefix(path, "/auth/logout") || 246 path == "/styles.css" || path == "/main.js" { 247 h(w, r) 248 return 249 } 250 authRequired(h)(w, r) 251 } 252 } 253 } 254 255 func getDashboardData() (serviceCount, runningCount, stoppedCount int, statusDot string) { 256 homeDir, err := os.UserHomeDir() 257 if err != nil { 258 return 259 } 260 pidDir := homeDir + "/micro/run" 261 dirEntries, err := os.ReadDir(pidDir) 262 if err != nil { 263 return 264 } 265 for _, entry := range dirEntries { 266 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") { 267 continue 268 } 269 pidFile := pidDir + "/" + entry.Name() 270 pidBytes, err := os.ReadFile(pidFile) 271 if err != nil { 272 continue 273 } 274 lines := strings.Split(string(pidBytes), "\n") 275 pid := "-" 276 if len(lines) > 0 && len(lines[0]) > 0 { 277 pid = lines[0] 278 } 279 serviceCount++ 280 if pid != "-" { 281 if _, err := os.FindProcess(parsePid(pid)); err == nil { 282 if processRunning(pid) { 283 runningCount++ 284 } else { 285 stoppedCount++ 286 } 287 } else { 288 stoppedCount++ 289 } 290 } else { 291 stoppedCount++ 292 } 293 } 294 if serviceCount > 0 && runningCount == serviceCount { 295 statusDot = "green" 296 } else if serviceCount > 0 && runningCount > 0 { 297 statusDot = "yellow" 298 } else { 299 statusDot = "red" 300 } 301 return 302 } 303 304 func getSidebarEndpoints() ([]map[string]string, error) { 305 apiCache.Lock() 306 defer apiCache.Unlock() 307 if apiCache.data != nil && time.Since(apiCache.time) < 30*time.Second { 308 if v, ok := apiCache.data["SidebarEndpoints"]; ok { 309 if endpoints, ok := v.([]map[string]string); ok { 310 return endpoints, nil 311 } 312 } 313 } 314 services, err := registry.ListServices() 315 if err != nil { 316 return nil, err 317 } 318 var sidebarEndpoints []map[string]string 319 for _, srv := range services { 320 anchor := strings.ReplaceAll(srv.Name, ".", "-") 321 sidebarEndpoints = append(sidebarEndpoints, map[string]string{"Name": srv.Name, "Anchor": anchor}) 322 } 323 sort.Slice(sidebarEndpoints, func(i, j int) bool { 324 return sidebarEndpoints[i]["Name"] < sidebarEndpoints[j]["Name"] 325 }) 326 return sidebarEndpoints, nil 327 } 328 329 func registerHandlers(tmpls *templates, storeInst store.Store) { 330 authMw := authRequired(storeInst) 331 wrap := wrapAuth(authMw) 332 333 // Serve static files from root (not /html/) with correct Content-Type 334 http.HandleFunc("/styles.css", func(w http.ResponseWriter, r *http.Request) { 335 w.Header().Set("Content-Type", "text/css; charset=utf-8") 336 f, err := HTML.Open("html/styles.css") 337 if err != nil { 338 w.WriteHeader(404) 339 return 340 } 341 defer f.Close() 342 io.Copy(w, f) 343 }) 344 345 http.HandleFunc("/main.js", func(w http.ResponseWriter, r *http.Request) { 346 w.Header().Set("Content-Type", "application/javascript; charset=utf-8") 347 f, err := HTML.Open("html/main.js") 348 if err != nil { 349 w.WriteHeader(404) 350 return 351 } 352 defer f.Close() 353 io.Copy(w, f) 354 }) 355 356 // Serve /html/styles.css and /html/main.js for compatibility 357 http.HandleFunc("/html/styles.css", func(w http.ResponseWriter, r *http.Request) { 358 w.Header().Set("Content-Type", "text/css; charset=utf-8") 359 f, err := HTML.Open("html/styles.css") 360 if err != nil { 361 w.WriteHeader(404) 362 return 363 } 364 defer f.Close() 365 io.Copy(w, f) 366 }) 367 http.HandleFunc("/html/main.js", func(w http.ResponseWriter, r *http.Request) { 368 w.Header().Set("Content-Type", "application/javascript; charset=utf-8") 369 f, err := HTML.Open("html/main.js") 370 if err != nil { 371 w.WriteHeader(404) 372 return 373 } 374 defer f.Close() 375 io.Copy(w, f) 376 }) 377 378 http.HandleFunc("/", wrap(func(w http.ResponseWriter, r *http.Request) { 379 path := r.URL.Path 380 if strings.HasPrefix(path, "/auth/") { 381 // Let the dedicated /auth/* handlers process this 382 return 383 } 384 userID := getUser(r) 385 var user any 386 if userID != "" { 387 user = &TemplateUser{ID: userID} 388 } else { 389 user = nil 390 } 391 if path == "/" { 392 serviceCount, runningCount, stoppedCount, statusDot := getDashboardData() 393 // Do NOT include SidebarEndpoints on home page 394 err := tmpls.home.Execute(w, map[string]any{ 395 "Title": "Micro Dashboard", 396 "WebLink": "/", 397 "ServiceCount": serviceCount, 398 "RunningCount": runningCount, 399 "StoppedCount": stoppedCount, 400 "StatusDot": statusDot, 401 "User": user, 402 // No SidebarEndpoints or SidebarEndpointsEnabled here 403 }) 404 if err != nil { 405 log.Printf("[TEMPLATE ERROR] home: %v", err) 406 } 407 return 408 } 409 if path == "/api" || path == "/api/" { 410 apiCache.Lock() 411 useCache := false 412 if apiCache.data != nil && time.Since(apiCache.time) < 30*time.Second { 413 useCache = true 414 } 415 var apiData map[string]any 416 var sidebarEndpoints []map[string]string 417 if useCache { 418 apiData = apiCache.data 419 if v, ok := apiData["SidebarEndpoints"]; ok { 420 sidebarEndpoints, _ = v.([]map[string]string) 421 } 422 } else { 423 services, _ := registry.ListServices() 424 var apiServices []map[string]any 425 for _, srv := range services { 426 srvs, err := registry.GetService(srv.Name) 427 if err != nil || len(srvs) == 0 { 428 continue 429 } 430 s := srvs[0] 431 if len(s.Endpoints) == 0 { 432 continue 433 } 434 endpoints := []map[string]any{} 435 for _, ep := range s.Endpoints { 436 parts := strings.Split(ep.Name, ".") 437 if len(parts) != 2 { 438 continue 439 } 440 apiPath := fmt.Sprintf("/api/%s/%s/%s", s.Name, parts[0], parts[1]) 441 var params, response string 442 if ep.Request != nil && len(ep.Request.Values) > 0 { 443 params += "<ul class=no-bullets>" 444 for _, v := range ep.Request.Values { 445 params += fmt.Sprintf("<li><b>%s</b> <span style='color:#888;'>%s</span></li>", v.Name, v.Type) 446 } 447 params += "</ul>" 448 } else { 449 params = "<i style='color:#888;'>No parameters</i>" 450 } 451 if ep.Response != nil && len(ep.Response.Values) > 0 { 452 response += "<ul class=no-bullets>" 453 for _, v := range ep.Response.Values { 454 response += fmt.Sprintf("<li><b>%s</b> <span style='color:#888;'>%s</span></li>", v.Name, v.Type) 455 } 456 response += "</ul>" 457 } else { 458 response = "<i style='color:#888;'>No response fields</i>" 459 } 460 endpoints = append(endpoints, map[string]any{ 461 "Name": ep.Name, 462 "Path": apiPath, 463 "Params": params, 464 "Response": response, 465 }) 466 } 467 anchor := strings.ReplaceAll(s.Name, ".", "-") 468 apiServices = append(apiServices, map[string]any{ 469 "Name": s.Name, 470 "Anchor": anchor, 471 "Endpoints": endpoints, 472 }) 473 sidebarEndpoints = append(sidebarEndpoints, map[string]string{"Name": s.Name, "Anchor": anchor}) 474 } 475 sort.Slice(sidebarEndpoints, func(i, j int) bool { 476 return sidebarEndpoints[i]["Name"] < sidebarEndpoints[j]["Name"] 477 }) 478 apiData = map[string]any{"Title": "API", "WebLink": "/", "Services": apiServices, "SidebarEndpoints": sidebarEndpoints, "SidebarEndpointsEnabled": true, "User": user} 479 apiCache.data = apiData 480 apiCache.time = time.Now() 481 } 482 apiCache.Unlock() 483 // Add API auth doc at the top 484 apiData["ApiAuthDoc"] = `<div style='background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.08em;'> 485 <b>API Authentication Required:</b> All API calls to <code>/api/...</code> endpoints (except this page) must include an <b>Authorization: Bearer <token></b> header. <br> 486 You can generate tokens on the <a href='/auth/tokens'>Tokens page</a>. 487 </div>` 488 _ = render(w, tmpls.api, apiData) 489 return 490 } 491 if path == "/services" { 492 // Do NOT include SidebarEndpoints on this page 493 services, _ := registry.ListServices() 494 var serviceNames []string 495 for _, service := range services { 496 serviceNames = append(serviceNames, service.Name) 497 } 498 sort.Strings(serviceNames) 499 _ = render(w, tmpls.service, map[string]any{"Title": "Services", "WebLink": "/", "Services": serviceNames, "User": user}) 500 return 501 } 502 if path == "/logs" || path == "/logs/" { 503 // Do NOT include SidebarEndpoints on this page 504 homeDir, err := os.UserHomeDir() 505 if err != nil { 506 w.WriteHeader(500) 507 w.Write([]byte("Could not get home directory")) 508 return 509 } 510 logsDir := homeDir + "/micro/logs" 511 dirEntries, err := os.ReadDir(logsDir) 512 if err != nil { 513 w.WriteHeader(500) 514 w.Write([]byte("Could not list logs directory: " + err.Error())) 515 return 516 } 517 serviceNames := []string{} 518 for _, entry := range dirEntries { 519 name := entry.Name() 520 if !entry.IsDir() && strings.HasSuffix(name, ".log") && !strings.HasPrefix(name, ".") { 521 serviceNames = append(serviceNames, strings.TrimSuffix(name, ".log")) 522 } 523 } 524 _ = render(w, tmpls.logs, map[string]any{"Title": "Logs", "WebLink": "/", "Services": serviceNames, "User": user}) 525 return 526 } 527 if strings.HasPrefix(path, "/logs/") { 528 // Do NOT include SidebarEndpoints on this page 529 service := strings.TrimPrefix(path, "/logs/") 530 if service == "" { 531 w.WriteHeader(404) 532 w.Write([]byte("Service not specified")) 533 return 534 } 535 homeDir, err := os.UserHomeDir() 536 if err != nil { 537 w.WriteHeader(500) 538 w.Write([]byte("Could not get home directory")) 539 return 540 } 541 logFilePath := homeDir + "/micro/logs/" + service + ".log" 542 f, err := os.Open(logFilePath) 543 if err != nil { 544 w.WriteHeader(404) 545 w.Write([]byte("Could not open log file for service: " + service)) 546 return 547 } 548 defer f.Close() 549 logBytes, err := io.ReadAll(f) 550 if err != nil { 551 w.WriteHeader(500) 552 w.Write([]byte("Could not read log file for service: " + service)) 553 return 554 } 555 logText := string(logBytes) 556 _ = render(w, tmpls.log, map[string]any{"Title": "Logs for " + service, "WebLink": "/logs", "Service": service, "Log": logText, "User": user}) 557 return 558 } 559 if path == "/status" { 560 // Do NOT include SidebarEndpoints on this page 561 homeDir, err := os.UserHomeDir() 562 if err != nil { 563 w.WriteHeader(500) 564 w.Write([]byte("Could not get home directory")) 565 return 566 } 567 pidDir := homeDir + "/micro/run" 568 dirEntries, err := os.ReadDir(pidDir) 569 if err != nil { 570 w.WriteHeader(500) 571 w.Write([]byte("Could not list pid directory: " + err.Error())) 572 return 573 } 574 statuses := []map[string]string{} 575 for _, entry := range dirEntries { 576 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") { 577 continue 578 } 579 pidFile := pidDir + "/" + entry.Name() 580 pidBytes, err := os.ReadFile(pidFile) 581 if err != nil { 582 statuses = append(statuses, map[string]string{ 583 "Service": entry.Name(), 584 "Dir": "-", 585 "Status": "unknown", 586 "PID": "-", 587 "Uptime": "-", 588 "ID": strings.TrimSuffix(entry.Name(), ".pid"), 589 }) 590 continue 591 } 592 lines := strings.Split(string(pidBytes), "\n") 593 pid := "-" 594 dir := "-" 595 service := "-" 596 start := "-" 597 if len(lines) > 0 && len(lines[0]) > 0 { 598 pid = lines[0] 599 } 600 if len(lines) > 1 && len(lines[1]) > 0 { 601 dir = lines[1] 602 } 603 if len(lines) > 2 && len(lines[2]) > 0 { 604 service = lines[2] 605 } 606 if len(lines) > 3 && len(lines[3]) > 0 { 607 start = lines[3] 608 } 609 status := "stopped" 610 if pid != "-" { 611 if _, err := os.FindProcess(parsePid(pid)); err == nil { 612 if processRunning(pid) { 613 status = "running" 614 } 615 } else { 616 status = "stopped" 617 } 618 } 619 uptime := "-" 620 if start != "-" { 621 if t, err := parseStartTime(start); err == nil { 622 uptime = time.Since(t).Truncate(time.Second).String() 623 } 624 } 625 statuses = append(statuses, map[string]string{ 626 "Service": service, 627 "Dir": dir, 628 "Status": status, 629 "PID": pid, 630 "Uptime": uptime, 631 "ID": strings.TrimSuffix(entry.Name(), ".pid"), 632 }) 633 } 634 _ = render(w, tmpls.status, map[string]any{"Title": "Service Status", "WebLink": "/", "Statuses": statuses, "User": user}) 635 return 636 } 637 // Match /{service} and /{service}/{endpoint} 638 parts := strings.Split(strings.Trim(path, "/"), "/") 639 if len(parts) >= 1 && parts[0] != "api" && parts[0] != "html" && parts[0] != "services" { 640 service := parts[0] 641 if len(parts) == 1 { 642 s, err := registry.GetService(service) 643 if err != nil || len(s) == 0 { 644 w.WriteHeader(404) 645 w.Write([]byte(fmt.Sprintf("Service not found: %s", service))) 646 return 647 } 648 endpoints := []map[string]string{} 649 for _, ep := range s[0].Endpoints { 650 endpoints = append(endpoints, map[string]string{ 651 "Name": ep.Name, 652 "Path": fmt.Sprintf("/%s/%s", service, ep.Name), 653 }) 654 } 655 b, _ := json.MarshalIndent(s[0], "", " ") 656 _ = render(w, tmpls.service, map[string]any{ 657 "Title": "Service: " + service, 658 "WebLink": "/", 659 "ServiceName": service, 660 "Endpoints": endpoints, 661 "Description": string(b), 662 "User": user, 663 }) 664 return 665 } 666 if len(parts) == 2 { 667 service := parts[0] 668 endpoint := parts[1] // Use the actual endpoint name from the URL, e.g. Foo.Bar 669 s, err := registry.GetService(service) 670 if err != nil || len(s) == 0 { 671 w.WriteHeader(404) 672 w.Write([]byte("Service not found: " + service)) 673 return 674 } 675 var ep *registry.Endpoint 676 for _, eps := range s[0].Endpoints { 677 if eps.Name == endpoint { 678 ep = eps 679 break 680 } 681 } 682 if ep == nil { 683 w.WriteHeader(404) 684 w.Write([]byte("Endpoint not found")) 685 return 686 } 687 if r.Method == "GET" { 688 // Build form fields from endpoint request values 689 var inputs []map[string]string 690 if ep.Request != nil && len(ep.Request.Values) > 0 { 691 for _, input := range ep.Request.Values { 692 inputs = append(inputs, map[string]string{ 693 "Label": input.Name, 694 "Name": input.Name, 695 "Placeholder": input.Name, 696 "Value": "", 697 }) 698 } 699 } 700 _ = render(w, tmpls.form, map[string]any{ 701 "Title": "Service: " + service, 702 "WebLink": "/", 703 "ServiceName": service, 704 "EndpointName": ep.Name, 705 "Inputs": inputs, 706 "Action": service + "/" + endpoint, 707 "User": user, 708 }) 709 return 710 } 711 if r.Method == "POST" { 712 // Parse form values into a map 713 var reqBody map[string]interface{} 714 if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { 715 defer r.Body.Close() 716 json.NewDecoder(r.Body).Decode(&reqBody) 717 } else { 718 reqBody = map[string]interface{}{} 719 r.ParseForm() 720 for k, v := range r.Form { 721 if len(v) == 1 { 722 if len(v[0]) == 0 { 723 continue 724 } 725 reqBody[k] = v[0] 726 } else { 727 reqBody[k] = v 728 } 729 } 730 } 731 // For now, just echo the request body as JSON 732 w.Header().Set("Content-Type", "application/json") 733 b, _ := json.MarshalIndent(reqBody, "", " ") 734 w.Write(b) 735 return 736 } 737 } 738 } 739 w.WriteHeader(404) 740 w.Write([]byte("Not found")) 741 })) 742 http.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { 743 http.SetCookie(w, &http.Cookie{Name: "micro_token", Value: "", Path: "/", Expires: time.Now().Add(-1 * time.Hour), HttpOnly: true}) 744 http.Redirect(w, r, "/auth/login", http.StatusSeeOther) 745 }) 746 http.HandleFunc("/auth/tokens", authMw(func(w http.ResponseWriter, r *http.Request) { 747 userID := getUser(r) 748 var user any 749 if userID != "" { 750 user = &TemplateUser{ID: userID} 751 } else { 752 user = nil 753 } 754 if r.Method == "POST" { 755 id := r.FormValue("id") 756 typeStr := r.FormValue("type") 757 scopesStr := r.FormValue("scopes") 758 accType := "user" 759 if typeStr == "admin" { 760 accType = "admin" 761 } else if typeStr == "service" { 762 accType = "service" 763 } 764 scopes := []string{"*"} 765 if scopesStr != "" { 766 scopes = strings.Split(scopesStr, ",") 767 for i := range scopes { 768 scopes[i] = strings.TrimSpace(scopes[i]) 769 } 770 } 771 acc := &Account{ 772 ID: id, 773 Type: accType, 774 Scopes: scopes, 775 Metadata: map[string]string{"created": time.Now().Format(time.RFC3339)}, 776 } 777 // Service tokens do not require a password, generate a JWT directly 778 tok, _ := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour) 779 acc.Metadata["token"] = tok 780 b, _ := json.Marshal(acc) 781 storeInst.Write(&store.Record{Key: "auth/" + id, Value: b}) 782 storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token 783 http.Redirect(w, r, "/auth/tokens", http.StatusSeeOther) 784 return 785 } 786 recs, _ := storeInst.Read("auth/", store.ReadPrefix()) 787 var tokens []map[string]any 788 for _, rec := range recs { 789 var acc Account 790 if err := json.Unmarshal(rec.Value, &acc); err == nil { 791 tok := "" 792 if t, ok := acc.Metadata["token"]; ok { 793 tok = t 794 } 795 var tokenPrefix, tokenSuffix string 796 if len(tok) > 12 { 797 tokenPrefix = tok[:4] 798 tokenSuffix = tok[len(tok)-4:] 799 } else { 800 tokenPrefix = tok 801 tokenSuffix = "" 802 } 803 tokens = append(tokens, map[string]any{ 804 "ID": acc.ID, 805 "Type": acc.Type, 806 "Scopes": acc.Scopes, 807 "Metadata": acc.Metadata, 808 "Token": tok, 809 "TokenPrefix": tokenPrefix, 810 "TokenSuffix": tokenSuffix, 811 }) 812 } 813 } 814 _ = tmpls.authTokens.Execute(w, map[string]any{"Title": "Auth Tokens", "Tokens": tokens, "User": user, "Sub": userID}) 815 })) 816 817 http.HandleFunc("/auth/users", authMw(func(w http.ResponseWriter, r *http.Request) { 818 userID := getUser(r) 819 var user any 820 if userID != "" { 821 user = &TemplateUser{ID: userID} 822 } else { 823 user = nil 824 } 825 if r.Method == "POST" { 826 if del := r.FormValue("delete"); del != "" { 827 // Delete user 828 storeInst.Delete("auth/" + del) 829 deleteUserTokens(storeInst, del) // Delete all JWT tokens for this user 830 http.Redirect(w, r, "/auth/users", http.StatusSeeOther) 831 return 832 } 833 id := r.FormValue("id") 834 if id == "" { 835 http.Redirect(w, r, "/auth/users", http.StatusSeeOther) 836 return 837 } 838 pass := r.FormValue("password") 839 typeStr := r.FormValue("type") 840 accType := "user" 841 if typeStr == "admin" { 842 accType = "admin" 843 } 844 hash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) 845 acc := &Account{ 846 ID: id, 847 Type: accType, 848 Scopes: []string{"*"}, 849 Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)}, 850 } 851 b, _ := json.Marshal(acc) 852 storeInst.Write(&store.Record{Key: "auth/" + id, Value: b}) 853 http.Redirect(w, r, "/auth/users", http.StatusSeeOther) 854 return 855 } 856 recs, _ := storeInst.Read("auth/", store.ReadPrefix()) 857 var users []Account 858 for _, rec := range recs { 859 var acc Account 860 if err := json.Unmarshal(rec.Value, &acc); err == nil { 861 if acc.Type == "user" || acc.Type == "admin" { 862 users = append(users, acc) 863 } 864 } 865 } 866 _ = tmpls.authUsers.Execute(w, map[string]any{"Title": "User Accounts", "Users": users, "User": user}) 867 })) 868 http.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) { 869 if r.Method == "GET" { 870 loginTmpl, err := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html") 871 if err != nil { 872 w.WriteHeader(500) 873 w.Write([]byte("Template error: " + err.Error())) 874 return 875 } 876 _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "", "User": getUser(r), "HideSidebar": true}) 877 return 878 } 879 if r.Method == "POST" { 880 id := r.FormValue("id") 881 pass := r.FormValue("password") 882 recKey := "auth/" + id 883 recs, _ := storeInst.Read(recKey) 884 if len(recs) == 0 { 885 loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html") 886 _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) 887 return 888 } 889 var acc Account 890 if err := json.Unmarshal(recs[0].Value, &acc); err != nil { 891 loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html") 892 _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) 893 return 894 } 895 hash, ok := acc.Metadata["password_hash"] 896 if !ok || bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) != nil { 897 loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html") 898 _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true}) 899 return 900 } 901 tok, err := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour) 902 if err != nil { 903 log.Printf("[LOGIN ERROR] Token generation failed: %v\nAccount: %+v", err, acc) 904 loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html") 905 _ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Token error", "User": "", "HideSidebar": true}) 906 return 907 } 908 storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token 909 http.SetCookie(w, &http.Cookie{ 910 Name: "micro_token", 911 Value: tok, 912 Path: "/", 913 Expires: time.Now().Add(time.Hour * 24), 914 HttpOnly: true, 915 }) 916 http.Redirect(w, r, "/", http.StatusSeeOther) 917 return 918 } 919 w.WriteHeader(405) 920 w.Write([]byte("Method not allowed")) 921 }) 922 } 923 924 func Run(c *cli.Context) error { 925 if err := initAuth(); err != nil { 926 log.Fatalf("Failed to initialize auth: %v", err) 927 } 928 homeDir, _ := os.UserHomeDir() 929 keyDir := filepath.Join(homeDir, "micro", "keys") 930 privPath := filepath.Join(keyDir, "private.pem") 931 pubPath := filepath.Join(keyDir, "public.pem") 932 if err := InitJWTKeys(privPath, pubPath); err != nil { 933 log.Fatalf("Failed to init JWT keys: %v", err) 934 } 935 storeInst := store.DefaultStore 936 tmpls := parseTemplates() 937 registerHandlers(tmpls, storeInst) 938 addr := c.String("address") 939 if addr == "" { 940 addr = ":8080" 941 } 942 log.Printf("[micro-server] Web/API listening on %s", addr) 943 if err := http.ListenAndServe(addr, nil); err != nil { 944 log.Fatalf("Web/API server error: %v", err) 945 } 946 return nil 947 } 948 949 // --- PID FILES --- 950 // --- PID FILES --- 951 func parsePid(pidStr string) int { 952 pid, _ := strconv.Atoi(pidStr) 953 return pid 954 } 955 func processRunning(pid string) bool { 956 proc, err := os.FindProcess(parsePid(pid)) 957 if err != nil { 958 return false 959 } 960 // On unix, sending syscall.Signal(0) checks if process exists 961 return proc.Signal(syscall.Signal(0)) == nil 962 } 963 964 func generateKeyPair(bits int) (*rsa.PrivateKey, error) { 965 priv, err := rsa.GenerateKey(rand.Reader, bits) 966 if err != nil { 967 return nil, err 968 } 969 return priv, nil 970 } 971 func exportPrivateKeyAsPEM(priv *rsa.PrivateKey) ([]byte, error) { 972 privKeyBytes := x509.MarshalPKCS1PrivateKey(priv) 973 block := &pem.Block{ 974 Type: "RSA PRIVATE KEY", 975 Bytes: privKeyBytes, 976 } 977 var buf bytes.Buffer 978 err := pem.Encode(&buf, block) 979 if err != nil { 980 return nil, err 981 } 982 return buf.Bytes(), nil 983 } 984 func exportPublicKeyAsPEM(pub *rsa.PublicKey) ([]byte, error) { 985 pubKeyBytes := x509.MarshalPKCS1PublicKey(pub) 986 block := &pem.Block{ 987 Type: "RSA PUBLIC KEY", 988 Bytes: pubKeyBytes, 989 } 990 var buf bytes.Buffer 991 err := pem.Encode(&buf, block) 992 if err != nil { 993 return nil, err 994 } 995 return buf.Bytes(), nil 996 } 997 func importPrivateKeyFromPEM(privKeyPEM []byte) (*rsa.PrivateKey, error) { 998 block, _ := pem.Decode(privKeyPEM) 999 if block == nil { 1000 return nil, fmt.Errorf("invalid PEM block") 1001 } 1002 return x509.ParsePKCS1PrivateKey(block.Bytes) 1003 } 1004 func importPublicKeyFromPEM(pubKeyPEM []byte) (*rsa.PublicKey, error) { 1005 block, _ := pem.Decode(pubKeyPEM) 1006 if block == nil { 1007 return nil, fmt.Errorf("invalid PEM block") 1008 } 1009 return x509.ParsePKCS1PublicKey(block.Bytes) 1010 } 1011 func initAuth() error { 1012 // --- AUTH SETUP --- 1013 homeDir, _ := os.UserHomeDir() 1014 keyDir := filepath.Join(homeDir, "micro", "keys") 1015 privPath := filepath.Join(keyDir, "private.pem") 1016 pubPath := filepath.Join(keyDir, "public.pem") 1017 os.MkdirAll(keyDir, 0700) 1018 // Generate keypair if not exist 1019 if _, err := os.Stat(privPath); os.IsNotExist(err) { 1020 priv, _ := rsa.GenerateKey(rand.Reader, 2048) 1021 privBytes := x509.MarshalPKCS1PrivateKey(priv) 1022 privPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}) 1023 os.WriteFile(privPath, privPem, 0600) 1024 // Use PKIX format for public key 1025 pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) 1026 pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}) 1027 os.WriteFile(pubPath, pubPem, 0644) 1028 } 1029 _, _ = os.ReadFile(privPath) 1030 _, _ = os.ReadFile(pubPath) 1031 storeInst := store.DefaultStore 1032 // --- Ensure default admin account exists --- 1033 adminID := "admin" 1034 adminPass := "micro" 1035 adminKey := "auth/" + adminID 1036 if recs, _ := storeInst.Read(adminKey); len(recs) == 0 { 1037 // Hash the admin password with bcrypt 1038 hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost) 1039 if err != nil { 1040 return err 1041 } 1042 acc := &Account{ 1043 ID: adminID, 1044 Type: "admin", 1045 Scopes: []string{"*"}, 1046 Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)}, 1047 } 1048 b, _ := json.Marshal(acc) 1049 storeInst.Write(&store.Record{Key: adminKey, Value: b}) 1050 } 1051 return nil 1052 } 1053 1054 // parseStartTime parses a string as RFC3339 time 1055 func parseStartTime(s string) (time.Time, error) { 1056 return time.Parse(time.RFC3339, s) 1057 } 1058 func init() { 1059 cmd.Register(&cli.Command{ 1060 Name: "server", 1061 Usage: "Run the micro server", 1062 Action: Run, 1063 Flags: []cli.Flag{ 1064 &cli.StringFlag{ 1065 Name: "address", 1066 Usage: "Address to listen on", 1067 EnvVars: []string{"MICRO_SERVER_ADDRESS"}, 1068 Value: ":8080", 1069 }, 1070 }, 1071 }) 1072 }