1// Copyright (C) 2019 Storj Labs, Inc. 2// See LICENSE for copying information. 3 4package consoleweb 5 6import ( 7 "context" 8 "crypto/subtle" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "html/template" 13 "mime" 14 "net" 15 "net/http" 16 "net/url" 17 "os" 18 "path/filepath" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/gorilla/mux" 24 "github.com/graphql-go/graphql" 25 "github.com/graphql-go/graphql/gqlerrors" 26 "github.com/spacemonkeygo/monkit/v3" 27 "github.com/zeebo/errs" 28 "go.uber.org/zap" 29 "golang.org/x/sync/errgroup" 30 31 "storj.io/common/errs2" 32 "storj.io/common/memory" 33 "storj.io/common/storj" 34 "storj.io/common/uuid" 35 "storj.io/storj/private/web" 36 "storj.io/storj/satellite/analytics" 37 "storj.io/storj/satellite/console" 38 "storj.io/storj/satellite/console/consoleauth" 39 "storj.io/storj/satellite/console/consoleweb/consoleapi" 40 "storj.io/storj/satellite/console/consoleweb/consoleql" 41 "storj.io/storj/satellite/console/consoleweb/consolewebauth" 42 "storj.io/storj/satellite/mailservice" 43 "storj.io/storj/satellite/payments/paymentsconfig" 44 "storj.io/storj/satellite/rewards" 45) 46 47const ( 48 contentType = "Content-Type" 49 50 applicationJSON = "application/json" 51 applicationGraphql = "application/graphql" 52) 53 54var ( 55 // Error is satellite console error type. 56 Error = errs.Class("consoleweb") 57 58 mon = monkit.Package() 59) 60 61// Config contains configuration for console web server. 62type Config struct { 63 Address string `help:"server address of the graphql api gateway and frontend app" devDefault:"127.0.0.1:0" releaseDefault:":10100"` 64 StaticDir string `help:"path to static resources" default:""` 65 Watch bool `help:"whether to load templates on each request" default:"false" devDefault:"true"` 66 ExternalAddress string `help:"external endpoint of the satellite if hosted" default:""` 67 68 // TODO: remove after Vanguard release 69 AuthToken string `help:"auth token needed for access to registration token creation endpoint" default:"" testDefault:"very-secret-token"` 70 AuthTokenSecret string `help:"secret used to sign auth tokens" releaseDefault:"" devDefault:"my-suppa-secret-key"` 71 72 ContactInfoURL string `help:"url link to contacts page" default:"https://forum.storj.io"` 73 FrameAncestors string `help:"allow domains to embed the satellite in a frame, space separated" default:"tardigrade.io storj.io"` 74 LetUsKnowURL string `help:"url link to let us know page" default:"https://storjlabs.atlassian.net/servicedesk/customer/portals"` 75 SEO string `help:"used to communicate with web crawlers and other web robots" default:"User-agent: *\nDisallow: \nDisallow: /cgi-bin/"` 76 SatelliteName string `help:"used to display at web satellite console" default:"Storj"` 77 SatelliteOperator string `help:"name of organization which set up satellite" default:"Storj Labs" ` 78 TermsAndConditionsURL string `help:"url link to terms and conditions page" default:"https://storj.io/storage-sla/"` 79 AccountActivationRedirectURL string `help:"url link for account activation redirect" default:""` 80 PartneredSatellites SatList `help:"names and addresses of partnered satellites in JSON list format" default:"[[\"US1\",\"https://us1.storj.io\"],[\"EU1\",\"https://eu1.storj.io\"],[\"AP1\",\"https://ap1.storj.io\"]]"` 81 GeneralRequestURL string `help:"url link to general request page" default:"https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000379291"` 82 ProjectLimitsIncreaseRequestURL string `help:"url link to project limit increase request page" default:"https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212"` 83 GatewayCredentialsRequestURL string `help:"url link for gateway credentials requests" default:"https://auth.us1.storjshare.io" devDefault:""` 84 IsBetaSatellite bool `help:"indicates if satellite is in beta" default:"false"` 85 BetaSatelliteFeedbackURL string `help:"url link for for beta satellite feedback" default:""` 86 BetaSatelliteSupportURL string `help:"url link for for beta satellite support" default:""` 87 DocumentationURL string `help:"url link to documentation" default:"https://docs.storj.io/"` 88 CouponCodeBillingUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account from billing" default:"false"` 89 CouponCodeSignupUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account from signup" default:"false"` 90 FileBrowserFlowDisabled bool `help:"indicates if file browser flow is disabled" default:"false"` 91 CSPEnabled bool `help:"indicates if Content Security Policy is enabled" devDefault:"false" releaseDefault:"true"` 92 LinksharingURL string `help:"url link for linksharing requests" default:"https://link.us1.storjshare.io"` 93 PathwayOverviewEnabled bool `help:"indicates if the overview onboarding step should render with pathways" default:"true"` 94 NewProjectDashboard bool `help:"indicates if new project dashboard should be used" default:"false"` 95 NewOnboarding bool `help:"indicates if new onboarding flow should be rendered" default:"true"` 96 NewNavigation bool `help:"indicates if new navigation structure should be rendered" default:"true"` 97 NewBrowser bool `help:"indicates if new browser should be used" default:"true"` 98 NewObjectsFlow bool `help:"indicates if new objects flow should be used" default:"true"` 99 100 // RateLimit defines the configuration for the IP and userID rate limiters. 101 RateLimit web.RateLimiterConfig 102 103 console.Config 104} 105 106// SatList is a configuration value that contains a list of satellite names and addresses. 107// Format should be [[name,address],[name,address],...] in valid JSON format. 108// 109// Can be used as a flag. 110type SatList string 111 112// Type implements pflag.Value. 113func (SatList) Type() string { return "consoleweb.SatList" } 114 115// String is required for pflag.Value. 116func (sl *SatList) String() string { 117 return string(*sl) 118} 119 120// Set does validation on the configured JSON, but does not actually transform it - it will be passed to the client as-is. 121func (sl *SatList) Set(s string) error { 122 satellites := make([][]string, 3) 123 124 err := json.Unmarshal([]byte(s), &satellites) 125 if err != nil { 126 return err 127 } 128 129 for _, sat := range satellites { 130 if len(sat) != 2 { 131 return errs.New("Could not parse satellite list config. Each satellite in the config must have two values: [name, address]") 132 } 133 } 134 135 *sl = SatList(s) 136 return nil 137} 138 139// Server represents console web server. 140// 141// architecture: Endpoint 142type Server struct { 143 log *zap.Logger 144 145 config Config 146 service *console.Service 147 mailService *mailservice.Service 148 partners *rewards.PartnersService 149 analytics *analytics.Service 150 151 listener net.Listener 152 server http.Server 153 cookieAuth *consolewebauth.CookieAuth 154 ipRateLimiter *web.RateLimiter 155 userIDRateLimiter *web.RateLimiter 156 nodeURL storj.NodeURL 157 158 stripePublicKey string 159 160 pricing paymentsconfig.PricingValues 161 162 schema graphql.Schema 163 164 templatesCache *templates 165} 166 167type templates struct { 168 index *template.Template 169 notFound *template.Template 170 internalServerError *template.Template 171 usageReport *template.Template 172} 173 174// NewServer creates new instance of console server. 175func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, listener net.Listener, stripePublicKey string, pricing paymentsconfig.PricingValues, nodeURL storj.NodeURL) *Server { 176 server := Server{ 177 log: logger, 178 config: config, 179 listener: listener, 180 service: service, 181 mailService: mailService, 182 partners: partners, 183 analytics: analytics, 184 stripePublicKey: stripePublicKey, 185 ipRateLimiter: web.NewIPRateLimiter(config.RateLimit), 186 userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit), 187 nodeURL: nodeURL, 188 pricing: pricing, 189 } 190 191 logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr())) 192 193 server.cookieAuth = consolewebauth.NewCookieAuth(consolewebauth.CookieSettings{ 194 Name: "_tokenKey", 195 Path: "/", 196 }) 197 198 if server.config.ExternalAddress != "" { 199 if !strings.HasSuffix(server.config.ExternalAddress, "/") { 200 server.config.ExternalAddress += "/" 201 } 202 } else { 203 server.config.ExternalAddress = "http://" + server.listener.Addr().String() + "/" 204 } 205 206 if server.config.AccountActivationRedirectURL == "" { 207 server.config.AccountActivationRedirectURL = server.config.ExternalAddress + "login?activated=true" 208 } 209 210 router := mux.NewRouter() 211 fs := http.FileServer(http.Dir(server.config.StaticDir)) 212 213 router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler) 214 router.HandleFunc("/robots.txt", server.seoHandler) 215 216 router.Handle("/api/v0/graphql", server.withAuth(http.HandlerFunc(server.graphqlHandler))) 217 218 usageLimitsController := consoleapi.NewUsageLimits(logger, service) 219 router.Handle( 220 "/api/v0/projects/{id}/usage-limits", 221 server.withAuth(http.HandlerFunc(usageLimitsController.ProjectUsageLimits)), 222 ).Methods(http.MethodGet) 223 router.Handle( 224 "/api/v0/projects/usage-limits", 225 server.withAuth(http.HandlerFunc(usageLimitsController.TotalUsageLimits)), 226 ).Methods(http.MethodGet) 227 228 authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.analytics, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL) 229 authRouter := router.PathPrefix("/api/v0/auth").Subrouter() 230 authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet) 231 authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.UpdateAccount))).Methods(http.MethodPatch) 232 authRouter.Handle("/account/change-email", server.withAuth(http.HandlerFunc(authController.ChangeEmail))).Methods(http.MethodPost) 233 authRouter.Handle("/account/change-password", server.withAuth(http.HandlerFunc(authController.ChangePassword))).Methods(http.MethodPost) 234 authRouter.Handle("/account/delete", server.withAuth(http.HandlerFunc(authController.DeleteAccount))).Methods(http.MethodPost) 235 authRouter.Handle("/mfa/enable", server.withAuth(http.HandlerFunc(authController.EnableUserMFA))).Methods(http.MethodPost) 236 authRouter.Handle("/mfa/disable", server.withAuth(http.HandlerFunc(authController.DisableUserMFA))).Methods(http.MethodPost) 237 authRouter.Handle("/mfa/generate-secret-key", server.withAuth(http.HandlerFunc(authController.GenerateMFASecretKey))).Methods(http.MethodPost) 238 authRouter.Handle("/mfa/generate-recovery-codes", server.withAuth(http.HandlerFunc(authController.GenerateMFARecoveryCodes))).Methods(http.MethodPost) 239 authRouter.HandleFunc("/logout", authController.Logout).Methods(http.MethodPost) 240 authRouter.Handle("/token", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Token))).Methods(http.MethodPost) 241 authRouter.Handle("/register", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Register))).Methods(http.MethodPost, http.MethodOptions) 242 authRouter.Handle("/forgot-password/{email}", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ForgotPassword))).Methods(http.MethodPost) 243 authRouter.Handle("/resend-email/{id}", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResendEmail))).Methods(http.MethodPost) 244 authRouter.Handle("/reset-password", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResetPassword))).Methods(http.MethodPost) 245 246 paymentController := consoleapi.NewPayments(logger, service) 247 paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter() 248 paymentsRouter.Use(server.withAuth) 249 paymentsRouter.HandleFunc("/cards", paymentController.AddCreditCard).Methods(http.MethodPost) 250 paymentsRouter.HandleFunc("/cards", paymentController.MakeCreditCardDefault).Methods(http.MethodPatch) 251 paymentsRouter.HandleFunc("/cards", paymentController.ListCreditCards).Methods(http.MethodGet) 252 paymentsRouter.HandleFunc("/cards/{cardId}", paymentController.RemoveCreditCard).Methods(http.MethodDelete) 253 paymentsRouter.HandleFunc("/account/charges", paymentController.ProjectsCharges).Methods(http.MethodGet) 254 paymentsRouter.HandleFunc("/account/balance", paymentController.AccountBalance).Methods(http.MethodGet) 255 paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost) 256 paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet) 257 paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost) 258 paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch) 259 paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet) 260 261 bucketsController := consoleapi.NewBuckets(logger, service) 262 bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter() 263 bucketsRouter.Use(server.withAuth) 264 bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet) 265 266 apiKeysController := consoleapi.NewAPIKeys(logger, service) 267 apiKeysRouter := router.PathPrefix("/api/v0/api-keys").Subrouter() 268 apiKeysRouter.Use(server.withAuth) 269 apiKeysRouter.HandleFunc("/delete-by-name", apiKeysController.DeleteByNameAndProjectID).Methods(http.MethodDelete) 270 271 analyticsController := consoleapi.NewAnalytics(logger, service, server.analytics) 272 analyticsRouter := router.PathPrefix("/api/v0/analytics").Subrouter() 273 analyticsRouter.Use(server.withAuth) 274 analyticsRouter.HandleFunc("/event", analyticsController.EventTriggered).Methods(http.MethodPost) 275 276 if server.config.StaticDir != "" { 277 router.HandleFunc("/activation/", server.accountActivationHandler) 278 router.HandleFunc("/cancel-password-recovery/", server.cancelPasswordRecoveryHandler) 279 router.HandleFunc("/usage-report", server.bucketUsageReportHandler) 280 router.PathPrefix("/static/").Handler(server.brotliMiddleware(http.StripPrefix("/static", fs))) 281 router.PathPrefix("/").Handler(http.HandlerFunc(server.appHandler)) 282 } 283 284 server.server = http.Server{ 285 Handler: server.withRequest(router), 286 MaxHeaderBytes: ContentLengthLimit.Int(), 287 } 288 289 return &server 290} 291 292// Run starts the server that host webapp and api endpoint. 293func (server *Server) Run(ctx context.Context) (err error) { 294 defer mon.Task()(&ctx)(&err) 295 296 server.schema, err = consoleql.CreateSchema(server.log, server.service, server.mailService) 297 if err != nil { 298 return Error.Wrap(err) 299 } 300 301 _, err = server.loadTemplates() 302 if err != nil { 303 // TODO: should it return error if some template can not be initialized or just log about it? 304 return Error.Wrap(err) 305 } 306 307 ctx, cancel := context.WithCancel(ctx) 308 var group errgroup.Group 309 group.Go(func() error { 310 <-ctx.Done() 311 return server.server.Shutdown(context.Background()) 312 }) 313 group.Go(func() error { 314 server.ipRateLimiter.Run(ctx) 315 return nil 316 }) 317 group.Go(func() error { 318 defer cancel() 319 err := server.server.Serve(server.listener) 320 if errs2.IsCanceled(err) || errors.Is(err, http.ErrServerClosed) { 321 err = nil 322 } 323 return err 324 }) 325 326 return group.Wait() 327} 328 329// Close closes server and underlying listener. 330func (server *Server) Close() error { 331 return server.server.Close() 332} 333 334// appHandler is web app http handler function. 335func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) { 336 header := w.Header() 337 338 if server.config.CSPEnabled { 339 cspValues := []string{ 340 "default-src 'self'", 341 "connect-src 'self' *.tardigradeshare.io *.storjshare.io " + server.config.GatewayCredentialsRequestURL, 342 "frame-ancestors " + server.config.FrameAncestors, 343 "frame-src 'self' *.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/", 344 "img-src 'self' data: *.tardigradeshare.io *.storjshare.io", 345 "media-src 'self' *.tardigradeshare.io *.storjshare.io", 346 "script-src 'sha256-wAqYV6m2PHGd1WDyFBnZmSoyfCK0jxFAns0vGbdiWUA=' 'self' *.stripe.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/", 347 } 348 349 header.Set("Content-Security-Policy", strings.Join(cspValues, "; ")) 350 } 351 352 header.Set(contentType, "text/html; charset=UTF-8") 353 header.Set("X-Content-Type-Options", "nosniff") 354 header.Set("Referrer-Policy", "same-origin") // Only expose the referring url when navigating around the satellite itself. 355 356 var data struct { 357 ExternalAddress string 358 SatelliteName string 359 SatelliteNodeURL string 360 StripePublicKey string 361 PartneredSatellites string 362 DefaultProjectLimit int 363 GeneralRequestURL string 364 ProjectLimitsIncreaseRequestURL string 365 GatewayCredentialsRequestURL string 366 IsBetaSatellite bool 367 BetaSatelliteFeedbackURL string 368 BetaSatelliteSupportURL string 369 DocumentationURL string 370 CouponCodeBillingUIEnabled bool 371 CouponCodeSignupUIEnabled bool 372 FileBrowserFlowDisabled bool 373 LinksharingURL string 374 PathwayOverviewEnabled bool 375 StorageTBPrice string 376 EgressTBPrice string 377 SegmentPrice string 378 RecaptchaEnabled bool 379 RecaptchaSiteKey string 380 NewProjectDashboard bool 381 NewOnboarding bool 382 DefaultPaidStorageLimit memory.Size 383 DefaultPaidBandwidthLimit memory.Size 384 NewNavigation bool 385 NewBrowser bool 386 NewObjectsFlow bool 387 } 388 389 data.ExternalAddress = server.config.ExternalAddress 390 data.SatelliteName = server.config.SatelliteName 391 data.SatelliteNodeURL = server.nodeURL.String() 392 data.StripePublicKey = server.stripePublicKey 393 data.PartneredSatellites = string(server.config.PartneredSatellites) 394 data.DefaultProjectLimit = server.config.DefaultProjectLimit 395 data.GeneralRequestURL = server.config.GeneralRequestURL 396 data.ProjectLimitsIncreaseRequestURL = server.config.ProjectLimitsIncreaseRequestURL 397 data.GatewayCredentialsRequestURL = server.config.GatewayCredentialsRequestURL 398 data.IsBetaSatellite = server.config.IsBetaSatellite 399 data.BetaSatelliteFeedbackURL = server.config.BetaSatelliteFeedbackURL 400 data.BetaSatelliteSupportURL = server.config.BetaSatelliteSupportURL 401 data.DocumentationURL = server.config.DocumentationURL 402 data.CouponCodeBillingUIEnabled = server.config.CouponCodeBillingUIEnabled 403 data.CouponCodeSignupUIEnabled = server.config.CouponCodeSignupUIEnabled 404 data.FileBrowserFlowDisabled = server.config.FileBrowserFlowDisabled 405 data.LinksharingURL = server.config.LinksharingURL 406 data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled 407 data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid 408 data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid 409 data.StorageTBPrice = server.pricing.StorageTBPrice 410 data.EgressTBPrice = server.pricing.EgressTBPrice 411 data.SegmentPrice = server.pricing.SegmentPrice 412 data.RecaptchaEnabled = server.config.Recaptcha.Enabled 413 data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey 414 data.NewProjectDashboard = server.config.NewProjectDashboard 415 data.NewOnboarding = server.config.NewOnboarding 416 data.NewNavigation = server.config.NewNavigation 417 data.NewBrowser = server.config.NewBrowser 418 data.NewObjectsFlow = server.config.NewObjectsFlow 419 420 templates, err := server.loadTemplates() 421 if err != nil || templates.index == nil { 422 server.log.Error("unable to load templates", zap.Error(err)) 423 fmt.Fprintf(w, "Unable to load templates. See whether satellite UI has been built.") 424 return 425 } 426 427 if err := templates.index.Execute(w, data); err != nil { 428 server.log.Error("index template could not be executed", zap.Error(err)) 429 return 430 } 431} 432 433// authMiddlewareHandler performs initial authorization before every request. 434func (server *Server) withAuth(handler http.Handler) http.Handler { 435 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 436 var err error 437 var ctx context.Context 438 439 defer mon.Task()(&ctx)(&err) 440 441 ctxWithAuth := func(ctx context.Context) context.Context { 442 token, err := server.cookieAuth.GetToken(r) 443 if err != nil { 444 return console.WithAuthFailure(ctx, err) 445 } 446 447 ctx = consoleauth.WithAPIKey(ctx, []byte(token)) 448 449 auth, err := server.service.Authorize(ctx) 450 if err != nil { 451 return console.WithAuthFailure(ctx, err) 452 } 453 454 return console.WithAuth(ctx, auth) 455 } 456 457 ctx = ctxWithAuth(r.Context()) 458 459 handler.ServeHTTP(w, r.Clone(ctx)) 460 }) 461} 462 463// withRequest ensures the http request itself is reachable from the context. 464func (server *Server) withRequest(handler http.Handler) http.Handler { 465 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 466 handler.ServeHTTP(w, r.Clone(console.WithRequest(r.Context(), r))) 467 }) 468} 469 470// bucketUsageReportHandler generate bucket usage report page for project. 471func (server *Server) bucketUsageReportHandler(w http.ResponseWriter, r *http.Request) { 472 ctx := r.Context() 473 var err error 474 defer mon.Task()(&ctx)(&err) 475 476 token, err := server.cookieAuth.GetToken(r) 477 if err != nil { 478 server.serveError(w, http.StatusUnauthorized) 479 return 480 } 481 482 auth, err := server.service.Authorize(consoleauth.WithAPIKey(ctx, []byte(token))) 483 if err != nil { 484 server.serveError(w, http.StatusUnauthorized) 485 return 486 } 487 488 ctx = console.WithAuth(ctx, auth) 489 490 // parse query params 491 projectID, err := uuid.FromString(r.URL.Query().Get("projectID")) 492 if err != nil { 493 server.serveError(w, http.StatusBadRequest) 494 return 495 } 496 sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64) 497 if err != nil { 498 server.serveError(w, http.StatusBadRequest) 499 return 500 } 501 beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64) 502 if err != nil { 503 server.serveError(w, http.StatusBadRequest) 504 return 505 } 506 507 since := time.Unix(sinceStamp, 0).UTC() 508 before := time.Unix(beforeStamp, 0).UTC() 509 510 server.log.Debug("querying bucket usage report", 511 zap.Stringer("projectID", projectID), 512 zap.Stringer("since", since), 513 zap.Stringer("before", before)) 514 515 bucketRollups, err := server.service.GetBucketUsageRollups(ctx, projectID, since, before) 516 if err != nil { 517 server.log.Error("bucket usage report error", zap.Error(err)) 518 server.serveError(w, http.StatusInternalServerError) 519 return 520 } 521 522 templates, err := server.loadTemplates() 523 if err != nil { 524 server.log.Error("unable to load templates", zap.Error(err)) 525 return 526 } 527 if err = templates.usageReport.Execute(w, bucketRollups); err != nil { 528 server.log.Error("bucket usage report error", zap.Error(err)) 529 } 530} 531 532// createRegistrationTokenHandler is web app http handler function. 533func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) { 534 ctx := r.Context() 535 defer mon.Task()(&ctx)(nil) 536 w.Header().Set(contentType, applicationJSON) 537 538 var response struct { 539 Secret string `json:"secret"` 540 Error string `json:"error,omitempty"` 541 } 542 543 defer func() { 544 err := json.NewEncoder(w).Encode(&response) 545 if err != nil { 546 server.log.Error(err.Error()) 547 } 548 }() 549 550 equality := subtle.ConstantTimeCompare( 551 []byte(r.Header.Get("Authorization")), 552 []byte(server.config.AuthToken), 553 ) 554 if equality != 1 { 555 w.WriteHeader(401) 556 response.Error = "unauthorized" 557 return 558 } 559 560 projectsLimitInput := r.URL.Query().Get("projectsLimit") 561 562 projectsLimit, err := strconv.Atoi(projectsLimitInput) 563 if err != nil { 564 response.Error = err.Error() 565 return 566 } 567 568 token, err := server.service.CreateRegToken(ctx, projectsLimit) 569 if err != nil { 570 response.Error = err.Error() 571 return 572 } 573 574 response.Secret = token.Secret.String() 575} 576 577// accountActivationHandler is web app http handler function. 578func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Request) { 579 ctx := r.Context() 580 defer mon.Task()(&ctx)(nil) 581 activationToken := r.URL.Query().Get("token") 582 583 token, err := server.service.ActivateAccount(ctx, activationToken) 584 if err != nil { 585 server.log.Error("activation: failed to activate account", 586 zap.String("token", activationToken), 587 zap.Error(err)) 588 589 if console.ErrEmailUsed.Has(err) { 590 http.Redirect(w, r, server.config.ExternalAddress+"login?activated=false", http.StatusTemporaryRedirect) 591 return 592 } 593 594 if console.Error.Has(err) { 595 server.serveError(w, http.StatusInternalServerError) 596 return 597 } 598 599 server.serveError(w, http.StatusNotFound) 600 return 601 } 602 603 server.cookieAuth.SetTokenCookie(w, token) 604 605 http.Redirect(w, r, server.config.ExternalAddress, http.StatusTemporaryRedirect) 606} 607 608func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *http.Request) { 609 ctx := r.Context() 610 defer mon.Task()(&ctx)(nil) 611 recoveryToken := r.URL.Query().Get("token") 612 613 // No need to check error as we anyway redirect user to support page 614 _ = server.service.RevokeResetPasswordToken(ctx, recoveryToken) 615 616 // TODO: Should place this link to config 617 http.Redirect(w, r, "https://storjlabs.atlassian.net/servicedesk/customer/portals", http.StatusSeeOther) 618} 619 620// graphqlHandler is graphql endpoint http handler function. 621func (server *Server) graphqlHandler(w http.ResponseWriter, r *http.Request) { 622 ctx := r.Context() 623 defer mon.Task()(&ctx)(nil) 624 625 handleError := func(code int, err error) { 626 w.WriteHeader(code) 627 628 var jsonError struct { 629 Error string `json:"error"` 630 } 631 632 jsonError.Error = err.Error() 633 634 if err := json.NewEncoder(w).Encode(jsonError); err != nil { 635 server.log.Error("error graphql error", zap.Error(err)) 636 } 637 } 638 639 w.Header().Set(contentType, applicationJSON) 640 641 query, err := getQuery(w, r) 642 if err != nil { 643 handleError(http.StatusBadRequest, err) 644 return 645 } 646 647 rootObject := make(map[string]interface{}) 648 649 rootObject["origin"] = server.config.ExternalAddress 650 rootObject[consoleql.ActivationPath] = "activation/?token=" 651 rootObject[consoleql.PasswordRecoveryPath] = "password-recovery/?token=" 652 rootObject[consoleql.CancelPasswordRecoveryPath] = "cancel-password-recovery/?token=" 653 rootObject[consoleql.SignInPath] = "login" 654 rootObject[consoleql.LetUsKnowURL] = server.config.LetUsKnowURL 655 rootObject[consoleql.ContactInfoURL] = server.config.ContactInfoURL 656 rootObject[consoleql.TermsAndConditionsURL] = server.config.TermsAndConditionsURL 657 658 result := graphql.Do(graphql.Params{ 659 Schema: server.schema, 660 Context: ctx, 661 RequestString: query.Query, 662 VariableValues: query.Variables, 663 OperationName: query.OperationName, 664 RootObject: rootObject, 665 }) 666 667 getGqlError := func(err gqlerrors.FormattedError) error { 668 var gerr *gqlerrors.Error 669 if errors.As(err.OriginalError(), &gerr) { 670 return gerr.OriginalError 671 } 672 return nil 673 } 674 675 parseConsoleError := func(err error) (int, error) { 676 switch { 677 case console.ErrUnauthorized.Has(err): 678 return http.StatusUnauthorized, err 679 case console.Error.Has(err): 680 return http.StatusInternalServerError, err 681 } 682 683 return 0, nil 684 } 685 686 handleErrors := func(code int, errors gqlerrors.FormattedErrors) { 687 w.WriteHeader(code) 688 689 var jsonError struct { 690 Errors []string `json:"errors"` 691 } 692 693 for _, err := range errors { 694 jsonError.Errors = append(jsonError.Errors, err.Message) 695 } 696 697 if err := json.NewEncoder(w).Encode(jsonError); err != nil { 698 server.log.Error("error graphql error", zap.Error(err)) 699 } 700 } 701 702 handleGraphqlErrors := func() { 703 for _, err := range result.Errors { 704 gqlErr := getGqlError(err) 705 if gqlErr == nil { 706 continue 707 } 708 709 code, err := parseConsoleError(gqlErr) 710 if err != nil { 711 handleError(code, err) 712 return 713 } 714 } 715 716 handleErrors(http.StatusOK, result.Errors) 717 } 718 719 if result.HasErrors() { 720 handleGraphqlErrors() 721 return 722 } 723 724 err = json.NewEncoder(w).Encode(result) 725 if err != nil { 726 server.log.Error("error encoding grapql result", zap.Error(err)) 727 return 728 } 729 730 server.log.Debug(fmt.Sprintf("%s", result)) 731} 732 733// serveError serves error static pages. 734func (server *Server) serveError(w http.ResponseWriter, status int) { 735 w.WriteHeader(status) 736 737 switch status { 738 case http.StatusInternalServerError: 739 templates, err := server.loadTemplates() 740 if err != nil { 741 server.log.Error("unable to load templates", zap.Error(err)) 742 return 743 } 744 err = templates.internalServerError.Execute(w, nil) 745 if err != nil { 746 server.log.Error("cannot parse internalServerError template", zap.Error(err)) 747 } 748 case http.StatusNotFound: 749 templates, err := server.loadTemplates() 750 if err != nil { 751 server.log.Error("unable to load templates", zap.Error(err)) 752 return 753 } 754 err = templates.notFound.Execute(w, nil) 755 if err != nil { 756 server.log.Error("cannot parse pageNotFound template", zap.Error(err)) 757 } 758 } 759} 760 761// seoHandler used to communicate with web crawlers and other web robots. 762func (server *Server) seoHandler(w http.ResponseWriter, req *http.Request) { 763 header := w.Header() 764 765 header.Set(contentType, mime.TypeByExtension(".txt")) 766 header.Set("X-Content-Type-Options", "nosniff") 767 768 _, err := w.Write([]byte(server.config.SEO)) 769 if err != nil { 770 server.log.Error(err.Error()) 771 } 772} 773 774// brotliMiddleware is used to compress static content using brotli to minify resources if browser support such decoding. 775func (server *Server) brotliMiddleware(fn http.Handler) http.Handler { 776 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 777 w.Header().Set("Cache-Control", "public, max-age=31536000") 778 w.Header().Set("X-Content-Type-Options", "nosniff") 779 780 isBrotliSupported := strings.Contains(r.Header.Get("Accept-Encoding"), "br") 781 if !isBrotliSupported { 782 fn.ServeHTTP(w, r) 783 return 784 } 785 786 info, err := os.Stat(server.config.StaticDir + strings.TrimPrefix(r.URL.Path, "/static") + ".br") 787 if err != nil { 788 fn.ServeHTTP(w, r) 789 return 790 } 791 792 extension := filepath.Ext(info.Name()[:len(info.Name())-3]) 793 w.Header().Set(contentType, mime.TypeByExtension(extension)) 794 w.Header().Set("Content-Encoding", "br") 795 796 newRequest := new(http.Request) 797 *newRequest = *r 798 newRequest.URL = new(url.URL) 799 *newRequest.URL = *r.URL 800 newRequest.URL.Path += ".br" 801 802 fn.ServeHTTP(w, newRequest) 803 }) 804} 805 806// loadTemplates is used to initialize all templates. 807func (server *Server) loadTemplates() (_ *templates, err error) { 808 if server.config.Watch { 809 return server.parseTemplates() 810 } 811 812 if server.templatesCache != nil { 813 return server.templatesCache, nil 814 } 815 816 templates, err := server.parseTemplates() 817 if err != nil { 818 return nil, Error.Wrap(err) 819 } 820 821 server.templatesCache = templates 822 return server.templatesCache, nil 823} 824 825func (server *Server) parseTemplates() (_ *templates, err error) { 826 var t templates 827 828 t.index, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "dist", "index.html")) 829 if err != nil { 830 server.log.Error("dist folder is not generated. use 'npm run build' command", zap.Error(err)) 831 // Loading index is optional. 832 } 833 834 t.usageReport, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "reports", "usageReport.html")) 835 if err != nil { 836 return &t, Error.Wrap(err) 837 } 838 839 t.notFound, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "404.html")) 840 if err != nil { 841 return &t, Error.Wrap(err) 842 } 843 844 t.internalServerError, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "errors", "500.html")) 845 if err != nil { 846 return &t, Error.Wrap(err) 847 } 848 849 return &t, nil 850} 851 852// NewUserIDRateLimiter constructs a RateLimiter that limits based on user ID. 853func NewUserIDRateLimiter(config web.RateLimiterConfig) *web.RateLimiter { 854 return web.NewRateLimiter(config, func(r *http.Request) (string, error) { 855 auth, err := console.GetAuth(r.Context()) 856 if err != nil { 857 return "", err 858 } 859 return auth.User.ID.String(), nil 860 }) 861} 862