home to your local SPACEGIRL 馃挮 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dev 671 lines 24 kB view raw
1package main 2 3import ( 4 "bufio" 5 "embed" 6 "errors" 7 "fmt" 8 stdLog "log" 9 "math" 10 "math/rand" 11 "net" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "time" 18 19 "arimelody-web/admin" 20 "arimelody-web/api" 21 "arimelody-web/colour" 22 "arimelody-web/controller" 23 "arimelody-web/cursor" 24 "arimelody-web/log" 25 "arimelody-web/model" 26 "arimelody-web/view" 27 28 "github.com/jmoiron/sqlx" 29 _ "github.com/lib/pq" 30 "golang.org/x/crypto/bcrypt" 31) 32 33// used for database migrations 34const DB_VERSION = 1 35 36const DEFAULT_PORT int64 = 8080 37const HRT_DATE int64 = 1756478697 38 39//go:embed "public" 40var publicFS embed.FS 41 42func main() { 43 fmt.Printf("made with <3 by ari melody\n\n") 44 45 app := model.AppState{ 46 Config: controller.GetConfig(), 47 Twitch: nil, 48 PublicFS: publicFS, 49 } 50 51 // initialise database connection 52 if app.Config.DB.Host == "" { 53 fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n") 54 os.Exit(1) 55 } 56 if app.Config.DB.Name == "" { 57 fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n") 58 os.Exit(1) 59 } 60 if app.Config.DB.User == "" { 61 fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n") 62 os.Exit(1) 63 } 64 if app.Config.DB.Pass == "" { 65 fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n") 66 os.Exit(1) 67 } 68 69 var err error 70 app.DB, err = sqlx.Connect( 71 "postgres", 72 fmt.Sprintf( 73 "host=%s port=%d user=%s dbname=%s password='%s' sslmode=disable", 74 app.Config.DB.Host, 75 app.Config.DB.Port, 76 app.Config.DB.User, 77 app.Config.DB.Name, 78 app.Config.DB.Pass, 79 ), 80 ) 81 if err != nil { 82 fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err) 83 os.Exit(1) 84 } 85 app.DB.SetConnMaxLifetime(time.Minute * 3) 86 app.DB.SetMaxOpenConns(10) 87 app.DB.SetMaxIdleConns(10) 88 defer app.DB.Close() 89 90 app.Log = log.Logger{ DB: app.DB } 91 92 // handle command arguments 93 if len(os.Args) > 1 { 94 arg := os.Args[1] 95 96 switch arg { 97 case "createTOTP": 98 if len(os.Args) < 4 { 99 fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for createTOTP.\n") 100 os.Exit(1) 101 } 102 username := os.Args[2] 103 totpName := os.Args[3] 104 105 account, err := controller.GetAccountByUsername(app.DB, username) 106 if err != nil { 107 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 108 os.Exit(1) 109 } 110 111 if account == nil { 112 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 113 os.Exit(1) 114 } 115 116 secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH) 117 totp := model.TOTP { 118 AccountID: account.ID, 119 Name: totpName, 120 Secret: string(secret), 121 } 122 123 err = controller.CreateTOTP(app.DB, &totp) 124 if err != nil { 125 if strings.HasPrefix(err.Error(), "pq: duplicate key") { 126 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" already has a TOTP method named \"%s\"!\n", account.Username, totp.Name) 127 os.Exit(1) 128 } 129 fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err) 130 os.Exit(1) 131 } 132 133 app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username) 134 url := controller.GenerateTOTPURI(account.Username, totp.Secret) 135 fmt.Printf("%s\n", url) 136 return 137 138 case "deleteTOTP": 139 if len(os.Args) < 4 { 140 fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for deleteTOTP.\n") 141 os.Exit(1) 142 } 143 username := os.Args[2] 144 totpName := os.Args[3] 145 146 account, err := controller.GetAccountByUsername(app.DB, username) 147 if err != nil { 148 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 149 os.Exit(1) 150 } 151 152 if account == nil { 153 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 154 os.Exit(1) 155 } 156 157 err = controller.DeleteTOTP(app.DB, account.ID, totpName) 158 if err != nil { 159 fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err) 160 os.Exit(1) 161 } 162 163 app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username) 164 fmt.Printf("TOTP method \"%s\" deleted.\n", totpName) 165 return 166 167 case "listTOTP": 168 if len(os.Args) < 3 { 169 fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for listTOTP.\n") 170 os.Exit(1) 171 } 172 username := os.Args[2] 173 174 account, err := controller.GetAccountByUsername(app.DB, username) 175 if err != nil { 176 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 177 os.Exit(1) 178 } 179 180 if account == nil { 181 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 182 os.Exit(1) 183 } 184 185 totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) 186 if err != nil { 187 fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP methods: %v\n", err) 188 os.Exit(1) 189 } 190 191 for i, totp := range totps { 192 fmt.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt) 193 } 194 if len(totps) == 0 { 195 fmt.Printf("\"%s\" has no TOTP methods.\n", account.Username) 196 } 197 return 198 199 case "testTOTP": 200 if len(os.Args) < 4 { 201 fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for testTOTP.\n") 202 os.Exit(1) 203 } 204 username := os.Args[2] 205 totpName := os.Args[3] 206 207 account, err := controller.GetAccountByUsername(app.DB, username) 208 if err != nil { 209 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 210 os.Exit(1) 211 } 212 213 if account == nil { 214 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 215 os.Exit(1) 216 } 217 218 totp, err := controller.GetTOTP(app.DB, account.ID, totpName) 219 if err != nil { 220 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch TOTP method \"%s\": %v\n", totpName, err) 221 os.Exit(1) 222 } 223 224 if totp == nil { 225 fmt.Fprintf(os.Stderr, "FATAL: TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username) 226 os.Exit(1) 227 } 228 229 code := controller.GenerateTOTP(totp.Secret, 0) 230 fmt.Printf("%s\n", code) 231 return 232 233 case "cleanTOTP": 234 err := controller.DeleteUnconfirmedTOTPs(app.DB) 235 if err != nil { 236 fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err) 237 os.Exit(1) 238 } 239 app.Log.Info(log.TYPE_ACCOUNT, "TOTP methods pruned via config utility.") 240 fmt.Printf("Cleaned up dangling TOTP methods successfully.\n") 241 return 242 243 case "createInvite": 244 fmt.Printf("Creating invite...\n") 245 invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24) 246 if err != nil { 247 fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err) 248 os.Exit(1) 249 } 250 251 app.Log.Info(log.TYPE_ACCOUNT, "Invite generted via config utility (%s).", invite.Code) 252 fmt.Printf( 253 "Here you go! This code expires in %d hours: %s\n", 254 int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())), 255 invite.Code, 256 ) 257 return 258 259 case "purgeInvites": 260 fmt.Printf("Deleting all invites...\n") 261 err := controller.DeleteAllInvites(app.DB) 262 if err != nil { 263 fmt.Fprintf(os.Stderr, "FATAL: Failed to delete invites: %v\n", err) 264 os.Exit(1) 265 } 266 267 app.Log.Info(log.TYPE_ACCOUNT, "Invites purged via config utility.") 268 fmt.Printf("Invites deleted successfully.\n") 269 return 270 271 case "listAccounts": 272 accounts, err := controller.GetAllAccounts(app.DB) 273 if err != nil { 274 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch accounts: %v\n", err) 275 os.Exit(1) 276 } 277 278 for _, account := range accounts { 279 email := "<none>" 280 if account.Email.Valid { email = account.Email.String } 281 fmt.Printf( 282 "User: %s\n" + 283 "\tID: %s\n" + 284 "\tEmail: %s\n" + 285 "\tCreated: %s\n" + 286 "\tLocked: %t\n", 287 account.Username, 288 account.ID, 289 email, 290 account.CreatedAt, 291 account.Locked, 292 ) 293 } 294 return 295 296 case "changePassword": 297 if len(os.Args) < 4 { 298 fmt.Fprintf(os.Stderr, "FATAL: `username` and `password` must be specified for changePassword\n") 299 os.Exit(1) 300 } 301 302 username := os.Args[2] 303 password := os.Args[3] 304 account, err := controller.GetAccountByUsername(app.DB, username) 305 if err != nil { 306 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 307 os.Exit(1) 308 } 309 if account == nil { 310 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 311 os.Exit(1) 312 } 313 314 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 315 if err != nil { 316 fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err) 317 os.Exit(1) 318 } 319 account.Password = string(hashedPassword) 320 err = controller.UpdateAccount(app.DB, account) 321 if err != nil { 322 fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err) 323 os.Exit(1) 324 } 325 326 app.Log.Info(log.TYPE_ACCOUNT, "Password for '%s' updated via config utility.", account.Username) 327 fmt.Printf("Password for \"%s\" updated successfully.\n", account.Username) 328 return 329 330 case "deleteAccount": 331 if len(os.Args) < 3 { 332 fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n") 333 os.Exit(1) 334 } 335 username := os.Args[2] 336 fmt.Printf("Deleting account \"%s\"...\n", username) 337 338 account, err := controller.GetAccountByUsername(app.DB, username) 339 if err != nil { 340 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 341 os.Exit(1) 342 } 343 344 if account == nil { 345 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 346 os.Exit(1) 347 } 348 349 fmt.Printf("You are about to delete \"%s\". Are you sure? (y/[N]): ", account.Username) 350 res := "" 351 fmt.Scanln(&res) 352 if !strings.HasPrefix(res, "y") { 353 return 354 } 355 356 err = controller.DeleteAccount(app.DB, account.ID) 357 if err != nil { 358 fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err) 359 os.Exit(1) 360 } 361 362 app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' deleted via config utility.", account.Username) 363 fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username) 364 return 365 366 case "lockAccount": 367 if len(os.Args) < 3 { 368 fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for lockAccount\n") 369 os.Exit(1) 370 } 371 username := os.Args[2] 372 fmt.Printf("Unlocking account \"%s\"...\n", username) 373 374 account, err := controller.GetAccountByUsername(app.DB, username) 375 if err != nil { 376 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 377 os.Exit(1) 378 } 379 380 if account == nil { 381 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 382 os.Exit(1) 383 } 384 385 err = controller.LockAccount(app.DB, account.ID) 386 if err != nil { 387 fmt.Fprintf(os.Stderr, "FATAL: Failed to lock account: %v\n", err) 388 os.Exit(1) 389 } 390 391 app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' locked via config utility.", account.Username) 392 fmt.Printf("Account \"%s\" locked successfully.\n", account.Username) 393 return 394 395 case "unlockAccount": 396 if len(os.Args) < 3 { 397 fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for unlockAccount\n") 398 os.Exit(1) 399 } 400 username := os.Args[2] 401 fmt.Printf("Unlocking account \"%s\"...\n", username) 402 403 account, err := controller.GetAccountByUsername(app.DB, username) 404 if err != nil { 405 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) 406 os.Exit(1) 407 } 408 409 if account == nil { 410 fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) 411 os.Exit(1) 412 } 413 414 err = controller.UnlockAccount(app.DB, account.ID) 415 if err != nil { 416 fmt.Fprintf(os.Stderr, "FATAL: Failed to unlock account: %v\n", err) 417 os.Exit(1) 418 } 419 420 app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username) 421 fmt.Printf("Account \"%s\" unlocked successfully.\n", account.Username) 422 return 423 424 case "logs": 425 // TODO: add log search parameters 426 logs, err := app.Log.Search([]log.LogLevel{}, []string{}, "", 100, 0) 427 if err != nil { 428 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch logs: %v\n", err) 429 os.Exit(1) 430 } 431 for _, item := range(logs) { 432 levelStr := "" 433 switch item.Level { 434 case log.LEVEL_INFO: 435 levelStr = "INFO" 436 case log.LEVEL_WARN: 437 levelStr = "WARN" 438 default: 439 levelStr = fmt.Sprintf("? (%d)", item.Level) 440 } 441 fmt.Printf("[%s] %s:\n\t[%s] %s: %s\n", item.CreatedAt.Format(time.UnixDate), item.ID, item.Type, levelStr, item.Content) 442 } 443 return 444 } 445 446 // command help 447 fmt.Print( 448 "Available commands:\n\n" + 449 "createTOTP <username> <name>:\n\tCreates a timed one-time passcode method.\n" + 450 "listTOTP <username>:\n\tLists an account's TOTP methods.\n" + 451 "deleteTOTP <username> <name>:\n\tDeletes an account's TOTP method.\n" + 452 "testTOTP <username> <name>:\n\tGenerates the code for an account's TOTP method.\n" + 453 "cleanTOTP:\n\tCleans up unconfirmed (dangling) TOTP methods.\n" + 454 "\n" + 455 "createInvite:\n\tCreates an invite code to register new accounts.\n" + 456 "purgeInvites:\n\tDeletes all available invite codes.\n" + 457 "listAccounts:\n\tLists all active accounts.\n", 458 "deleteAccount <username>:\n\tDeletes the account under `username`.\n", 459 "lockAccount <username>:\n\tLocks the account under `username`.\n", 460 "unlockAccount <username>:\n\tUnlocks the account under `username`.\n", 461 "logs:\n\tShows system logs.\n", 462 ) 463 return 464 } 465 466 // handle DB migrations 467 controller.CheckDBVersionAndMigrate(app.DB) 468 469 if app.Config.Twitch != nil { 470 err = controller.TwitchSetup(&app) 471 if err != nil { 472 fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err) 473 } 474 } 475 476 // initial invite code 477 accountsCount := 0 478 err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account") 479 if err != nil { panic(err) } 480 if accountsCount == 0 { 481 _, err := app.DB.Exec("DELETE FROM invite") 482 if err != nil { 483 fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err) 484 os.Exit(1) 485 } 486 487 invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24) 488 if err != nil { 489 fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err) 490 os.Exit(1) 491 } 492 493 fmt.Printf("No accounts exist! Generated invite code: %s\n", invite.Code) 494 } 495 496 // delete expired sessions 497 err = controller.DeleteExpiredSessions(app.DB) 498 if err != nil { 499 fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired sessions: %v\n", err) 500 os.Exit(1) 501 } 502 503 // delete expired invites 504 err = controller.DeleteExpiredInvites(app.DB) 505 if err != nil { 506 fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) 507 os.Exit(1) 508 } 509 510 // clean up unconfirmed TOTP methods 511 err = controller.DeleteUnconfirmedTOTPs(app.DB) 512 if err != nil { 513 fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err) 514 os.Exit(1) 515 } 516 517 go cursor.StartCursor(&app) 518 519 // start the web server! 520 mux := createServeMux(&app) 521 fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port) 522 stdLog.Fatal( 523 http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port), 524 CheckRequest(&app, HTTPLog(DefaultHeaders(mux))), 525 )) 526} 527 528func createServeMux(app *model.AppState) *http.ServeMux { 529 mux := http.NewServeMux() 530 531 mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app))) 532 mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) 533 mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app))) 534 mux.Handle("/uploads/", http.StripPrefix("/uploads", view.ServeFiles(filepath.Join(app.Config.DataDirectory, "uploads")))) 535 mux.Handle("/cursor-ws", cursor.Handler(app)) 536 mux.Handle("/", view.IndexHandler(app)) 537 538 return mux 539} 540 541var PoweredByStrings = []string{ 542 "nerd rage", 543 "estrogen", 544 "your mother", 545 "awesome powers beyond comprehension", 546 "jared", 547 "the weight of my sins", 548 "the arc reactor", 549 "AA batteries", 550 "15 euro solar panel from ebay", 551 "magnets, how do they work", 552 "a fax machine", 553 "dell optiplex", 554 "a trans girl's nintendo wii", 555 "BASS", 556 "electricity, duh", 557 "seven hamsters in a big wheel", 558 "girls", 559 "mzungu hosting", 560 "golang", 561 "the state of the world right now", 562 "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", 563 "the good folks at aperture science", 564 "free2play CDs", 565 "aridoodle", 566 "the love of creating", 567 "not for the sake of art; not for the sake of money; we like painting naked people", 568 "30 billion dollars in VC funding", 569} 570 571func CheckRequest(app *model.AppState, next http.Handler) http.Handler { 572 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 573 // requests with empty user agents are considered suspicious. 574 // every browser supplies them; hell, even curl supplies them. 575 // i only ever see null user-agents paired with malicious requests, 576 // so i'm canning them altogether. 577 if len(r.Header.Get("User-Agent")) == 0 { 578 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 579 return 580 } 581 582 // obviously .php requests these don't affect me, but these tend to be 583 // lazy wordpress intrusion attempts. if that's what you're about, i 584 // don't want you on my site. 585 if strings.HasSuffix(r.URL.Path, ".php") || 586 strings.HasSuffix(r.URL.Path, ".php7") { 587 http.NotFound(w, r) 588 fmt.Fprintf( 589 os.Stderr, 590 "WARN: Suspicious activity blocked: {\"path\":\"%s\",\"address\":\"%s\"}\n", 591 r.URL.Path, 592 r.RemoteAddr, 593 ) 594 return 595 } 596 597 next.ServeHTTP(w, r) 598 }) 599} 600 601func DefaultHeaders(next http.Handler) http.Handler { 602 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 603 w.Header().Add("Server", "ari melody webbed site") 604 w.Header().Add("Do-Not-Stab", "1") 605 w.Header().Add("X-Clacks-Overhead", "GNU Terry Pratchett") 606 w.Header().Add("X-Hacker", "spare me please") 607 w.Header().Add("X-Robots-TXT", "'; DROP TABLE pages;") 608 w.Header().Add("X-Thinking-With", "Portals") 609 w.Header().Add( 610 "X-Powered-By", 611 PoweredByStrings[rand.Intn(len(PoweredByStrings))], 612 ) 613 w.Header().Add( 614 "X-Days-Since-HRT", 615 fmt.Sprint(math.Round(time.Since(time.Unix(HRT_DATE, 0)).Hours() / 24)), 616 ) 617 next.ServeHTTP(w, r) 618 }) 619} 620 621type LoggingResponseWriter struct { 622 http.ResponseWriter 623 Status int 624} 625 626func (lrw *LoggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 627 hijack, ok := lrw.ResponseWriter.(http.Hijacker) 628 if !ok { 629 return nil, nil, errors.New("Server does not support hijacking\n") 630 } 631 return hijack.Hijack() 632} 633 634func (lrw *LoggingResponseWriter) WriteHeader(status int) { 635 lrw.Status = status 636 lrw.ResponseWriter.WriteHeader(status) 637} 638 639func HTTPLog(next http.Handler) http.Handler { 640 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 641 start := time.Now() 642 643 lrw := LoggingResponseWriter{w, http.StatusOK} 644 645 next.ServeHTTP(&lrw, r) 646 647 after := time.Now() 648 difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 649 elapsed := "<1" 650 if difference >= 1 { 651 elapsed = strconv.Itoa(difference) 652 } 653 654 statusColour := colour.Reset 655 656 if lrw.Status - 600 <= 0 { statusColour = colour.Red } 657 if lrw.Status - 500 <= 0 { statusColour = colour.Yellow } 658 if lrw.Status - 400 <= 0 { statusColour = colour.White } 659 if lrw.Status - 300 <= 0 { statusColour = colour.Green } 660 661 fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", 662 after.Format(time.UnixDate), 663 r.Method, 664 r.URL.Path, 665 statusColour, 666 lrw.Status, 667 colour.Reset, 668 elapsed, 669 r.Header.Get("User-Agent")) 670 }) 671}