2019-01-24 16:26:36 +00:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleweb
import (
"context"
2020-01-14 13:38:32 +00:00
"crypto/subtle"
2023-04-18 11:42:17 +01:00
_ "embed"
2019-01-24 16:26:36 +00:00
"encoding/json"
2020-04-16 16:50:22 +01:00
"errors"
2020-04-13 10:31:17 +01:00
"fmt"
2019-04-10 00:14:19 +01:00
"html/template"
2023-04-05 23:35:01 +01:00
"io/fs"
2019-01-24 16:26:36 +00:00
"net"
"net/http"
2023-05-17 19:18:54 +01:00
"net/http/httputil"
2019-08-08 13:12:39 +01:00
"net/url"
2019-11-12 13:05:35 +00:00
"os"
2019-01-24 16:26:36 +00:00
"path/filepath"
2019-03-19 17:55:43 +00:00
"strconv"
2019-03-26 15:56:16 +00:00
"strings"
2019-04-10 00:14:19 +01:00
"time"
2019-01-24 16:26:36 +00:00
2019-10-21 17:42:49 +01:00
"github.com/gorilla/mux"
2023-09-07 20:11:20 +01:00
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/gqlerrors"
2020-10-09 14:40:12 +01:00
"github.com/spacemonkeygo/monkit/v3"
2019-01-24 16:26:36 +00:00
"github.com/zeebo/errs"
"go.uber.org/zap"
2022-06-30 18:44:17 +01:00
"go.uber.org/zap/zapcore"
2019-02-06 13:19:14 +00:00
"golang.org/x/sync/errgroup"
2019-01-24 16:26:36 +00:00
2020-04-16 16:50:22 +01:00
"storj.io/common/errs2"
2023-06-28 14:06:32 +01:00
"storj.io/common/http/requestid"
2023-05-18 11:07:36 +01:00
"storj.io/common/memory"
2020-12-23 04:24:37 +00:00
"storj.io/common/storj"
2020-04-08 20:40:49 +01:00
"storj.io/storj/private/web"
2022-09-13 11:40:55 +01:00
"storj.io/storj/satellite/abtesting"
2021-03-23 15:52:34 +00:00
"storj.io/storj/satellite/analytics"
2019-01-24 16:26:36 +00:00
"storj.io/storj/satellite/console"
2019-10-17 15:42:18 +01:00
"storj.io/storj/satellite/console/consoleweb/consoleapi"
2023-09-07 20:11:20 +01:00
"storj.io/storj/satellite/console/consoleweb/consoleql"
2020-01-20 18:57:14 +00:00
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
2019-03-02 15:22:20 +00:00
"storj.io/storj/satellite/mailservice"
2022-02-03 20:49:38 +00:00
"storj.io/storj/satellite/oidc"
2023-01-26 18:31:13 +00:00
"storj.io/storj/satellite/payments/paymentsconfig"
2019-01-24 16:26:36 +00:00
)
const (
2020-01-20 18:57:14 +00:00
contentType = "Content-Type"
2019-01-24 16:26:36 +00:00
2023-09-07 20:11:20 +01:00
applicationJSON = "application/json"
applicationGraphql = "application/graphql"
2019-01-24 16:26:36 +00:00
)
2019-06-04 12:55:38 +01:00
var (
2020-08-11 15:50:01 +01:00
// Error is satellite console error type.
2021-04-28 09:06:17 +01:00
Error = errs . Class ( "consoleweb" )
2019-06-04 12:55:38 +01:00
mon = monkit . Package ( )
)
2019-01-24 16:26:36 +00:00
2020-07-16 15:18:02 +01:00
// Config contains configuration for console web server.
2019-01-24 16:26:36 +00:00
type Config struct {
2023-08-22 11:51:59 +01:00
Address string ` help:"server address of the http api gateway and frontend app" devDefault:"127.0.0.1:0" releaseDefault:":10100" `
2023-05-17 19:18:54 +01:00
FrontendAddress string ` help:"server address of the front-end app" devDefault:"127.0.0.1:0" releaseDefault:":10200" `
ExternalAddress string ` help:"external endpoint of the satellite if hosted" default:"" `
FrontendEnable bool ` help:"feature flag to toggle whether console back-end server should also serve front-end endpoints" default:"true" `
BackendReverseProxy string ` help:"the target URL of console back-end reverse proxy for local development when running a UI server" default:"" `
StaticDir string ` help:"path to static resources" default:"" `
Watch bool ` help:"whether to load templates on each request" default:"false" devDefault:"true" `
2019-02-05 17:31:53 +00:00
2023-08-02 18:29:42 +01:00
AuthToken string ` help:"auth token needed for access to registration token creation endpoint" default:"" testDefault:"very-secret-token" `
AuthTokenSecret string ` help:"secret used to sign auth tokens" releaseDefault:"" devDefault:"my-suppa-secret-key" `
AuthCookieDomain string ` help:"optional domain for cookies to use" default:"" `
2019-03-19 17:55:43 +00:00
2023-02-22 10:32:32 +00:00
ContactInfoURL string ` help:"url link to contacts page" default:"https://forum.storj.io" `
FrameAncestors string ` help:"allow domains to embed the satellite in a frame, space separated" default:"tardigrade.io storj.io" `
LetUsKnowURL string ` help:"url link to let us know page" default:"https://storjlabs.atlassian.net/servicedesk/customer/portals" `
SEO string ` help:"used to communicate with web crawlers and other web robots" default:"User-agent: *\nDisallow: \nDisallow: /cgi-bin/" `
SatelliteName string ` help:"used to display at web satellite console" default:"Storj" `
SatelliteOperator string ` help:"name of organization which set up satellite" default:"Storj Labs" `
TermsAndConditionsURL string ` help:"url link to terms and conditions page" default:"https://www.storj.io/terms-of-service/" `
AccountActivationRedirectURL string ` help:"url link for account activation redirect" default:"" `
PartneredSatellites Satellites ` help:"names and addresses of partnered satellites in JSON list format" default:"[ { \"name\":\"US1\",\"address\":\"https://us1.storj.io\"}, { \"name\":\"EU1\",\"address\":\"https://eu1.storj.io\"}, { \"name\":\"AP1\",\"address\":\"https://ap1.storj.io\"}]" `
GeneralRequestURL string ` help:"url link to general request page" default:"https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000379291" `
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" `
2023-05-15 21:18:34 +01:00
GatewayCredentialsRequestURL string ` help:"url link for gateway credentials requests" default:"https://auth.storjsatelliteshare.io" devDefault:"http://localhost:8000" `
2023-02-22 10:32:32 +00:00
IsBetaSatellite bool ` help:"indicates if satellite is in beta" default:"false" `
BetaSatelliteFeedbackURL string ` help:"url link for for beta satellite feedback" default:"" `
BetaSatelliteSupportURL string ` help:"url link for for beta satellite support" default:"" `
DocumentationURL string ` help:"url link to documentation" default:"https://docs.storj.io/" `
CouponCodeBillingUIEnabled bool ` help:"indicates if user is allowed to add coupon codes to account from billing" default:"false" `
CouponCodeSignupUIEnabled bool ` help:"indicates if user is allowed to add coupon codes to account from signup" default:"false" `
FileBrowserFlowDisabled bool ` help:"indicates if file browser flow is disabled" default:"false" `
CSPEnabled bool ` help:"indicates if Content Security Policy is enabled" devDefault:"false" releaseDefault:"true" `
2023-05-15 21:18:34 +01:00
LinksharingURL string ` help:"url link for linksharing requests within the application" default:"https://link.storjsatelliteshare.io" devDefault:"http://localhost:8001" `
PublicLinksharingURL string ` help:"url link for linksharing requests for external sharing" default:"https://link.storjshare.io" devDefault:"http://localhost:8001" `
2023-02-22 10:32:32 +00:00
PathwayOverviewEnabled bool ` help:"indicates if the overview onboarding step should render with pathways" default:"true" `
2023-06-14 21:51:29 +01:00
AllProjectsDashboard bool ` help:"indicates if all projects dashboard should be used" default:"true" `
2023-08-14 16:05:19 +01:00
LimitsAreaEnabled bool ` help:"indicates whether limit card section of the UI is enabled" default:"true" `
2023-02-22 10:32:32 +00:00
GeneratedAPIEnabled bool ` help:"indicates if generated console api should be used" default:"false" `
OptionalSignupSuccessURL string ` help:"optional url to external registration success page" default:"" `
HomepageURL string ` help:"url link to storj.io homepage" default:"https://www.storj.io" `
NativeTokenPaymentsEnabled bool ` help:"indicates if storj native token payments system is enabled" default:"false" `
2023-01-26 18:31:13 +00:00
PricingPackagesEnabled bool ` help:"whether to allow purchasing pricing packages" default:"false" devDefault:"true" `
2023-05-19 15:24:59 +01:00
NewUploadModalEnabled bool ` help:"whether to show new upload modal" default:"false" `
2023-08-14 16:05:19 +01:00
GalleryViewEnabled bool ` help:"whether to show new gallery view" default:"true" `
2023-06-08 14:52:40 +01:00
UseVuetifyProject bool ` help:"whether to use vuetify POC project" default:"false" `
2023-08-02 18:29:42 +01:00
VuetifyHost string ` help:"the subdomain the vuetify POC project should be hosted on" default:"" `
2023-08-31 12:33:39 +01:00
ObjectBrowserPaginationEnabled bool ` help:"whether to use object browser pagination" default:"false" `
2020-03-11 15:36:55 +00:00
2022-02-03 20:49:38 +00:00
OauthCodeExpiry time . Duration ` help:"how long oauth authorization codes are issued for" default:"10m" `
OauthAccessTokenExpiry time . Duration ` help:"how long oauth access tokens are issued for" default:"24h" `
OauthRefreshTokenExpiry time . Duration ` help:"how long oauth refresh tokens are issued for" default:"720h" `
2023-05-18 11:07:36 +01:00
BodySizeLimit memory . Size ` help:"The maximum body size allowed to be received by the API" default:"100.00 KB" `
2021-08-17 20:38:34 +01:00
// RateLimit defines the configuration for the IP and userID rate limiters.
RateLimit web . RateLimiterConfig
2020-04-08 20:40:49 +01:00
2022-09-13 11:40:55 +01:00
ABTesting abtesting . Config
2020-03-11 15:36:55 +00:00
console . Config
2019-01-24 16:26:36 +00:00
}
2020-12-05 16:01:42 +00:00
// Server represents console web server.
2019-09-10 14:24:16 +01:00
//
// architecture: Endpoint
2019-01-24 16:26:36 +00:00
type Server struct {
log * zap . Logger
2021-02-04 18:16:49 +00:00
config Config
service * console . Service
mailService * mailservice . Service
2021-03-23 15:52:34 +00:00
analytics * analytics . Service
2022-09-13 11:40:55 +01:00
abTesting * abtesting . Service
2019-03-02 15:22:20 +00:00
2021-08-17 20:38:34 +01:00
listener net . Listener
server http . Server
2023-06-12 21:25:53 +01:00
router * mux . Router
2021-08-17 20:38:34 +01:00
cookieAuth * consolewebauth . CookieAuth
ipRateLimiter * web . RateLimiter
userIDRateLimiter * web . RateLimiter
nodeURL storj . NodeURL
2019-01-24 16:26:36 +00:00
2023-07-10 17:53:39 +01:00
stripePublicKey string
neededTokenPaymentConfirmations int
2019-11-18 11:38:43 +00:00
2023-01-26 18:31:13 +00:00
packagePlans paymentsconfig . PackagePlans
2023-09-07 20:11:20 +01:00
schema graphql . Schema
2023-04-05 23:35:01 +01:00
errorTemplate * template . Template
2019-01-24 16:26:36 +00:00
}
2022-06-05 23:41:38 +01:00
// apiAuth exposes methods to control authentication process for each generated API endpoint.
type apiAuth struct {
server * Server
}
// IsAuthenticated checks if request is performed with all needed authorization credentials.
func ( a * apiAuth ) IsAuthenticated ( ctx context . Context , r * http . Request , isCookieAuth , isKeyAuth bool ) ( _ context . Context , err error ) {
if isCookieAuth && isKeyAuth {
ctx , err = a . cookieAuth ( ctx , r )
if err != nil {
ctx , err = a . keyAuth ( ctx , r )
if err != nil {
return nil , err
}
}
} else if isCookieAuth {
ctx , err = a . cookieAuth ( ctx , r )
if err != nil {
return nil , err
}
} else if isKeyAuth {
ctx , err = a . keyAuth ( ctx , r )
if err != nil {
return nil , err
}
}
return ctx , nil
}
// cookieAuth returns an authenticated context by session cookie.
func ( a * apiAuth ) cookieAuth ( ctx context . Context , r * http . Request ) ( context . Context , error ) {
2022-07-19 10:26:18 +01:00
tokenInfo , err := a . server . cookieAuth . GetToken ( r )
2022-06-05 23:41:38 +01:00
if err != nil {
return nil , err
}
2022-07-19 10:26:18 +01:00
return a . server . service . TokenAuth ( ctx , tokenInfo . Token , time . Now ( ) )
2022-06-05 23:41:38 +01:00
}
// cookieAuth returns an authenticated context by api key.
func ( a * apiAuth ) keyAuth ( ctx context . Context , r * http . Request ) ( context . Context , error ) {
authToken := r . Header . Get ( "Authorization" )
split := strings . Split ( authToken , "Bearer " )
if len ( split ) != 2 {
return ctx , errs . New ( "authorization key format is incorrect. Should be 'Bearer <key>'" )
}
return a . server . service . KeyAuth ( ctx , split [ 1 ] , time . Now ( ) )
}
// RemoveAuthCookie indicates to the client that the authentication cookie should be removed.
func ( a * apiAuth ) RemoveAuthCookie ( w http . ResponseWriter ) {
a . server . cookieAuth . RemoveTokenCookie ( w )
}
2019-10-17 15:42:18 +01:00
// NewServer creates new instance of console server.
2023-07-10 17:53:39 +01:00
func NewServer ( logger * zap . Logger , config Config , service * console . Service , oidcService * oidc . Service , mailService * mailservice . Service , analytics * analytics . Service , abTesting * abtesting . Service , accountFreezeService * console . AccountFreezeService , listener net . Listener , stripePublicKey string , neededTokenPaymentConfirmations int , nodeURL storj . NodeURL , packagePlans paymentsconfig . PackagePlans ) * Server {
2023-09-07 14:48:36 +01:00
initAdditionalMimeTypes ( )
2019-01-24 16:26:36 +00:00
server := Server {
2023-07-10 17:53:39 +01:00
log : logger ,
config : config ,
listener : listener ,
service : service ,
mailService : mailService ,
analytics : analytics ,
abTesting : abTesting ,
stripePublicKey : stripePublicKey ,
neededTokenPaymentConfirmations : neededTokenPaymentConfirmations ,
ipRateLimiter : web . NewIPRateLimiter ( config . RateLimit , logger ) ,
userIDRateLimiter : NewUserIDRateLimiter ( config . RateLimit , logger ) ,
nodeURL : nodeURL ,
packagePlans : packagePlans ,
2019-01-24 16:26:36 +00:00
}
2023-05-17 19:18:54 +01:00
logger . Debug ( "Starting Satellite Console server." , zap . Stringer ( "Address" , server . listener . Addr ( ) ) )
2019-02-28 20:12:52 +00:00
2020-01-20 18:57:14 +00:00
server . cookieAuth = consolewebauth . NewCookieAuth ( consolewebauth . CookieSettings {
Name : "_tokenKey" ,
Path : "/" ,
2023-08-02 18:29:42 +01:00
} , server . config . AuthCookieDomain )
2020-01-20 18:57:14 +00:00
2019-03-26 15:56:16 +00:00
if server . config . ExternalAddress != "" {
if ! strings . HasSuffix ( server . config . ExternalAddress , "/" ) {
2019-05-29 14:14:25 +01:00
server . config . ExternalAddress += "/"
2019-03-26 15:56:16 +00:00
}
} else {
server . config . ExternalAddress = "http://" + server . listener . Addr ( ) . String ( ) + "/"
}
2020-03-23 20:38:50 +00:00
if server . config . AccountActivationRedirectURL == "" {
server . config . AccountActivationRedirectURL = server . config . ExternalAddress + "login?activated=true"
}
2019-10-21 17:42:49 +01:00
router := mux . NewRouter ( )
2023-06-12 21:25:53 +01:00
server . router = router
2022-06-30 18:44:17 +01:00
// N.B. This middleware has to be the first one because it has to be called
// the earliest in the HTTP chain.
router . Use ( newTraceRequestMiddleware ( logger , router ) )
2019-10-22 17:17:09 +01:00
2023-06-28 14:06:32 +01:00
router . Use ( requestid . AddToContext )
2023-05-18 11:07:36 +01:00
// limit body size
router . Use ( newBodyLimiterMiddleware ( logger . Named ( "body-limiter-middleware" ) , config . BodySizeLimit ) )
2022-02-17 13:49:07 +00:00
if server . config . GeneratedAPIEnabled {
2022-06-16 03:07:38 +01:00
consoleapi . NewProjectManagement ( logger , mon , server . service , router , & apiAuth { & server } )
consoleapi . NewAPIKeyManagement ( logger , mon , server . service , router , & apiAuth { & server } )
consoleapi . NewUserManagement ( logger , mon , server . service , router , & apiAuth { & server } )
2022-02-17 13:49:07 +00:00
}
2023-06-12 21:25:53 +01:00
router . Handle ( "/api/v0/config" , server . withCORS ( http . HandlerFunc ( server . frontendConfigHandler ) ) )
2023-04-05 16:56:06 +01:00
2023-09-07 20:11:20 +01:00
router . Handle ( "/api/v0/graphql" , server . withCORS ( server . withAuth ( http . HandlerFunc ( server . graphqlHandler ) ) ) )
2019-10-21 17:42:49 +01:00
router . HandleFunc ( "/registrationToken/" , server . createRegistrationTokenHandler )
router . HandleFunc ( "/robots.txt" , server . seoHandler )
2023-06-01 08:27:09 +01:00
projectsController := consoleapi . NewProjects ( logger , service )
projectsRouter := router . PathPrefix ( "/api/v0/projects" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
projectsRouter . Use ( server . withCORS )
projectsRouter . Use ( server . withAuth )
2023-08-02 22:44:03 +01:00
projectsRouter . Handle ( "" , http . HandlerFunc ( projectsController . GetUserProjects ) ) . Methods ( http . MethodGet , http . MethodOptions )
2023-08-18 14:31:49 +01:00
projectsRouter . Handle ( "" , http . HandlerFunc ( projectsController . CreateProject ) ) . Methods ( http . MethodPost , http . MethodOptions )
2023-08-07 14:16:04 +01:00
projectsRouter . Handle ( "/paged" , http . HandlerFunc ( projectsController . GetPagedProjects ) ) . Methods ( http . MethodGet , http . MethodOptions )
2023-08-09 01:14:28 +01:00
projectsRouter . Handle ( "/{id}" , http . HandlerFunc ( projectsController . UpdateProject ) ) . Methods ( http . MethodPatch , http . MethodOptions )
2023-09-06 13:30:55 +01:00
projectsRouter . Handle ( "/{id}/limit-increase" , http . HandlerFunc ( projectsController . RequestLimitIncrease ) ) . Methods ( http . MethodPost , http . MethodOptions )
2023-08-07 17:16:53 +01:00
projectsRouter . Handle ( "/{id}/members" , http . HandlerFunc ( projectsController . DeleteMembersAndInvitations ) ) . Methods ( http . MethodDelete , http . MethodOptions )
2023-06-12 21:25:53 +01:00
projectsRouter . Handle ( "/{id}/salt" , http . HandlerFunc ( projectsController . GetSalt ) ) . Methods ( http . MethodGet , http . MethodOptions )
2023-08-02 16:32:53 +01:00
projectsRouter . Handle ( "/{id}/members" , http . HandlerFunc ( projectsController . GetMembersAndInvitations ) ) . Methods ( http . MethodGet , http . MethodOptions )
2023-06-12 21:25:53 +01:00
projectsRouter . Handle ( "/{id}/invite" , http . HandlerFunc ( projectsController . InviteUsers ) ) . Methods ( http . MethodPost , http . MethodOptions )
projectsRouter . Handle ( "/{id}/invite-link" , http . HandlerFunc ( projectsController . GetInviteLink ) ) . Methods ( http . MethodGet , http . MethodOptions )
projectsRouter . Handle ( "/invitations" , http . HandlerFunc ( projectsController . GetUserInvitations ) ) . Methods ( http . MethodGet , http . MethodOptions )
projectsRouter . Handle ( "/invitations/{id}/respond" , http . HandlerFunc ( projectsController . RespondToInvitation ) ) . Methods ( http . MethodPost , http . MethodOptions )
2020-01-20 18:57:14 +00:00
2021-06-24 16:49:15 +01:00
usageLimitsController := consoleapi . NewUsageLimits ( logger , service )
2023-06-12 21:25:53 +01:00
projectsRouter . Handle ( "/{id}/usage-limits" , http . HandlerFunc ( usageLimitsController . ProjectUsageLimits ) ) . Methods ( http . MethodGet , http . MethodOptions )
projectsRouter . Handle ( "/usage-limits" , http . HandlerFunc ( usageLimitsController . TotalUsageLimits ) ) . Methods ( http . MethodGet , http . MethodOptions )
projectsRouter . Handle ( "/{id}/daily-usage" , http . HandlerFunc ( usageLimitsController . DailyUsage ) ) . Methods ( http . MethodGet , http . MethodOptions )
2019-12-12 12:58:15 +00:00
2023-01-27 21:07:32 +00:00
authController := consoleapi . NewAuth ( logger , service , accountFreezeService , mailService , server . cookieAuth , server . analytics , config . SatelliteName , server . config . ExternalAddress , config . LetUsKnowURL , config . TermsAndConditionsURL , config . ContactInfoURL , config . GeneralRequestURL )
2019-10-23 18:33:24 +01:00
authRouter := router . PathPrefix ( "/api/v0/auth" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
authRouter . Use ( server . withCORS )
authRouter . Handle ( "/account" , server . withAuth ( http . HandlerFunc ( authController . GetAccount ) ) ) . Methods ( http . MethodGet , http . MethodOptions )
authRouter . Handle ( "/account" , server . withAuth ( http . HandlerFunc ( authController . UpdateAccount ) ) ) . Methods ( http . MethodPatch , http . MethodOptions )
authRouter . Handle ( "/account/change-email" , server . withAuth ( http . HandlerFunc ( authController . ChangeEmail ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/account/change-password" , server . withAuth ( server . userIDRateLimiter . Limit ( http . HandlerFunc ( authController . ChangePassword ) ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/account/freezestatus" , server . withAuth ( http . HandlerFunc ( authController . GetFreezeStatus ) ) ) . Methods ( http . MethodGet , http . MethodOptions )
authRouter . Handle ( "/account/settings" , server . withAuth ( http . HandlerFunc ( authController . GetUserSettings ) ) ) . Methods ( http . MethodGet , http . MethodOptions )
authRouter . Handle ( "/account/settings" , server . withAuth ( http . HandlerFunc ( authController . SetUserSettings ) ) ) . Methods ( http . MethodPatch , http . MethodOptions )
authRouter . Handle ( "/account/onboarding" , server . withAuth ( http . HandlerFunc ( authController . SetOnboardingStatus ) ) ) . Methods ( http . MethodPatch , http . MethodOptions )
authRouter . Handle ( "/account/delete" , server . withAuth ( http . HandlerFunc ( authController . DeleteAccount ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/mfa/enable" , server . withAuth ( http . HandlerFunc ( authController . EnableUserMFA ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/mfa/disable" , server . withAuth ( http . HandlerFunc ( authController . DisableUserMFA ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/mfa/generate-secret-key" , server . withAuth ( http . HandlerFunc ( authController . GenerateMFASecretKey ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/mfa/generate-recovery-codes" , server . withAuth ( http . HandlerFunc ( authController . GenerateMFARecoveryCodes ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
2023-09-14 13:35:03 +01:00
authRouter . Handle ( "/mfa/regenerate-recovery-codes" , server . withAuth ( http . HandlerFunc ( authController . RegenerateMFARecoveryCodes ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
2023-06-12 21:25:53 +01:00
authRouter . Handle ( "/logout" , server . withAuth ( http . HandlerFunc ( authController . Logout ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/token" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . Token ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/token-by-api-key" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . TokenByAPIKey ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
2021-08-17 20:38:34 +01:00
authRouter . Handle ( "/register" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . Register ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
2023-06-12 21:25:53 +01:00
authRouter . Handle ( "/forgot-password" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . ForgotPassword ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/resend-email/{email}" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . ResendEmail ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/reset-password" , server . ipRateLimiter . Limit ( http . HandlerFunc ( authController . ResetPassword ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
authRouter . Handle ( "/refresh-session" , server . withAuth ( http . HandlerFunc ( authController . RefreshSession ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
2019-10-21 13:48:29 +01:00
2022-09-13 11:40:55 +01:00
if config . ABTesting . Enabled {
abController := consoleapi . NewABTesting ( logger , abTesting )
abRouter := router . PathPrefix ( "/api/v0/ab" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
abRouter . Use ( server . withCORS )
abRouter . Use ( server . withAuth )
abRouter . Handle ( "/values" , http . HandlerFunc ( abController . GetABValues ) ) . Methods ( http . MethodGet , http . MethodOptions )
abRouter . Handle ( "/hit/{action}" , http . HandlerFunc ( abController . SendHit ) ) . Methods ( http . MethodPost , http . MethodOptions )
2022-09-13 11:40:55 +01:00
}
2023-01-26 18:31:13 +00:00
paymentController := consoleapi . NewPayments ( logger , service , accountFreezeService , packagePlans )
2019-10-23 18:33:24 +01:00
paymentsRouter := router . PathPrefix ( "/api/v0/payments" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
paymentsRouter . Use ( server . withCORS )
2019-10-23 18:33:24 +01:00
paymentsRouter . Use ( server . withAuth )
2023-06-12 21:25:53 +01:00
paymentsRouter . Handle ( "/cards" , server . userIDRateLimiter . Limit ( http . HandlerFunc ( paymentController . AddCreditCard ) ) ) . Methods ( http . MethodPost , http . MethodOptions )
paymentsRouter . HandleFunc ( "/cards" , paymentController . MakeCreditCardDefault ) . Methods ( http . MethodPatch , http . MethodOptions )
paymentsRouter . HandleFunc ( "/cards" , paymentController . ListCreditCards ) . Methods ( http . MethodGet , http . MethodOptions )
paymentsRouter . HandleFunc ( "/cards/{cardId}" , paymentController . RemoveCreditCard ) . Methods ( http . MethodDelete , http . MethodOptions )
paymentsRouter . HandleFunc ( "/account/charges" , paymentController . ProjectsCharges ) . Methods ( http . MethodGet , http . MethodOptions )
paymentsRouter . HandleFunc ( "/account/balance" , paymentController . AccountBalance ) . Methods ( http . MethodGet , http . MethodOptions )
paymentsRouter . HandleFunc ( "/account" , paymentController . SetupAccount ) . Methods ( http . MethodPost , http . MethodOptions )
paymentsRouter . HandleFunc ( "/wallet" , paymentController . GetWallet ) . Methods ( http . MethodGet , http . MethodOptions )
paymentsRouter . HandleFunc ( "/wallet" , paymentController . ClaimWallet ) . Methods ( http . MethodPost , http . MethodOptions )
paymentsRouter . HandleFunc ( "/wallet/payments" , paymentController . WalletPayments ) . Methods ( http . MethodGet , http . MethodOptions )
2023-07-10 17:53:39 +01:00
paymentsRouter . HandleFunc ( "/wallet/payments-with-confirmations" , paymentController . WalletPaymentsWithConfirmations ) . Methods ( http . MethodGet , http . MethodOptions )
2023-06-12 21:25:53 +01:00
paymentsRouter . HandleFunc ( "/billing-history" , paymentController . BillingHistory ) . Methods ( http . MethodGet , http . MethodOptions )
2023-08-28 16:59:29 +01:00
paymentsRouter . HandleFunc ( "/invoice-history" , paymentController . InvoiceHistory ) . Methods ( http . MethodGet , http . MethodOptions )
2023-06-12 21:25:53 +01:00
paymentsRouter . Handle ( "/coupon/apply" , server . userIDRateLimiter . Limit ( http . HandlerFunc ( paymentController . ApplyCouponCode ) ) ) . Methods ( http . MethodPatch , http . MethodOptions )
paymentsRouter . HandleFunc ( "/coupon" , paymentController . GetCoupon ) . Methods ( http . MethodGet , http . MethodOptions )
paymentsRouter . HandleFunc ( "/pricing" , paymentController . GetProjectUsagePriceModel ) . Methods ( http . MethodGet , http . MethodOptions )
2023-01-26 18:31:13 +00:00
if config . PricingPackagesEnabled {
2023-06-12 21:25:53 +01:00
paymentsRouter . HandleFunc ( "/purchase-package" , paymentController . PurchasePackage ) . Methods ( http . MethodPost , http . MethodOptions )
paymentsRouter . HandleFunc ( "/package-available" , paymentController . PackageAvailable ) . Methods ( http . MethodGet , http . MethodOptions )
2023-01-26 18:31:13 +00:00
}
2019-01-24 16:26:36 +00:00
2020-11-13 11:41:35 +00:00
bucketsController := consoleapi . NewBuckets ( logger , service )
bucketsRouter := router . PathPrefix ( "/api/v0/buckets" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
bucketsRouter . Use ( server . withCORS )
2020-11-13 11:41:35 +00:00
bucketsRouter . Use ( server . withAuth )
2023-06-12 21:25:53 +01:00
bucketsRouter . HandleFunc ( "/bucket-names" , bucketsController . AllBucketNames ) . Methods ( http . MethodGet , http . MethodOptions )
2023-08-08 06:32:25 +01:00
bucketsRouter . HandleFunc ( "/usage-totals" , bucketsController . GetBucketTotals ) . Methods ( http . MethodGet , http . MethodOptions )
2020-11-13 11:41:35 +00:00
2021-03-16 19:43:02 +00:00
apiKeysController := consoleapi . NewAPIKeys ( logger , service )
apiKeysRouter := router . PathPrefix ( "/api/v0/api-keys" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
apiKeysRouter . Use ( server . withCORS )
2021-03-16 19:43:02 +00:00
apiKeysRouter . Use ( server . withAuth )
2023-08-07 13:45:25 +01:00
apiKeysRouter . HandleFunc ( "/create/{projectID}" , apiKeysController . CreateAPIKey ) . Methods ( http . MethodPost , http . MethodOptions )
2023-08-04 15:45:13 +01:00
apiKeysRouter . HandleFunc ( "/list-paged" , apiKeysController . GetProjectAPIKeys ) . Methods ( http . MethodGet , http . MethodOptions )
2023-06-12 21:25:53 +01:00
apiKeysRouter . HandleFunc ( "/delete-by-name" , apiKeysController . DeleteByNameAndProjectID ) . Methods ( http . MethodDelete , http . MethodOptions )
2023-08-07 15:27:10 +01:00
apiKeysRouter . HandleFunc ( "/delete-by-ids" , apiKeysController . DeleteByIDs ) . Methods ( http . MethodDelete , http . MethodOptions )
2023-06-12 21:25:53 +01:00
apiKeysRouter . HandleFunc ( "/api-key-names" , apiKeysController . GetAllAPIKeyNames ) . Methods ( http . MethodGet , http . MethodOptions )
2021-03-16 19:43:02 +00:00
2021-03-31 19:34:44 +01:00
analyticsController := consoleapi . NewAnalytics ( logger , service , server . analytics )
analyticsRouter := router . PathPrefix ( "/api/v0/analytics" ) . Subrouter ( )
2023-06-12 21:25:53 +01:00
analyticsRouter . Use ( server . withCORS )
2021-03-31 19:34:44 +01:00
analyticsRouter . Use ( server . withAuth )
2023-06-12 21:25:53 +01:00
analyticsRouter . HandleFunc ( "/event" , analyticsController . EventTriggered ) . Methods ( http . MethodPost , http . MethodOptions )
analyticsRouter . HandleFunc ( "/page" , analyticsController . PageEventTriggered ) . Methods ( http . MethodPost , http . MethodOptions )
2021-03-31 19:34:44 +01:00
2023-05-17 19:18:54 +01:00
oidc := oidc . NewEndpoint (
server . nodeURL , server . config . ExternalAddress ,
logger , oidcService , service ,
server . config . OauthCodeExpiry , server . config . OauthAccessTokenExpiry , server . config . OauthRefreshTokenExpiry ,
)
2023-09-11 14:09:02 +01:00
router . HandleFunc ( "/api/v0/.well-known/openid-configuration" , oidc . WellKnownConfiguration )
router . Handle ( "/api/v0/oauth/v2/authorize" , server . withAuth ( http . HandlerFunc ( oidc . AuthorizeUser ) ) ) . Methods ( http . MethodPost )
router . Handle ( "/api/v0/oauth/v2/tokens" , server . ipRateLimiter . Limit ( http . HandlerFunc ( oidc . Tokens ) ) ) . Methods ( http . MethodPost )
router . Handle ( "/api/v0/oauth/v2/userinfo" , server . ipRateLimiter . Limit ( http . HandlerFunc ( oidc . UserInfo ) ) ) . Methods ( http . MethodGet )
router . Handle ( "/api/v0/oauth/v2/clients/{id}" , server . withAuth ( http . HandlerFunc ( oidc . GetClient ) ) ) . Methods ( http . MethodGet )
2022-02-03 20:49:38 +00:00
2023-05-17 19:18:54 +01:00
router . HandleFunc ( "/invited" , server . handleInvited )
router . HandleFunc ( "/activation" , server . accountActivationHandler )
router . HandleFunc ( "/cancel-password-recovery" , server . cancelPasswordRecoveryHandler )
2022-02-03 20:49:38 +00:00
2023-05-17 19:18:54 +01:00
if server . config . StaticDir != "" && server . config . FrontendEnable {
2022-02-03 20:49:38 +00:00
fs := http . FileServer ( http . Dir ( server . config . StaticDir ) )
2023-06-12 21:25:53 +01:00
router . PathPrefix ( "/static/" ) . Handler ( server . withCORS ( server . brotliMiddleware ( http . StripPrefix ( "/static" , fs ) ) ) )
2022-02-03 20:49:38 +00:00
2023-06-08 22:53:24 +01:00
if server . config . UseVuetifyProject {
2023-08-02 18:29:42 +01:00
if server . config . VuetifyHost != "" {
router . Host ( server . config . VuetifyHost ) . Handler ( server . withCORS ( http . HandlerFunc ( server . vuetifyAppHandler ) ) )
} else {
// if not using a custom subdomain for vuetify, use a path prefix on the same domain as the production app
router . PathPrefix ( "/vuetifypoc" ) . Handler ( server . withCORS ( http . HandlerFunc ( server . vuetifyAppHandler ) ) )
}
2023-06-08 22:53:24 +01:00
}
2023-06-12 21:25:53 +01:00
router . PathPrefix ( "/" ) . Handler ( server . withCORS ( http . HandlerFunc ( server . appHandler ) ) )
2019-01-24 16:26:36 +00:00
}
server . server = http . Server {
2020-09-06 02:56:07 +01:00
Handler : server . withRequest ( router ) ,
2019-09-20 18:40:26 +01:00
MaxHeaderBytes : ContentLengthLimit . Int ( ) ,
2019-01-24 16:26:36 +00:00
}
return & server
}
2020-01-24 13:38:53 +00:00
// Run starts the server that host webapp and api endpoint.
2019-08-13 13:37:01 +01:00
func ( server * Server ) Run ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2023-09-07 20:11:20 +01:00
server . schema , err = consoleql . CreateSchema ( server . log , server . service , server . mailService )
if err != nil {
return Error . Wrap ( err )
}
2023-04-05 23:35:01 +01:00
_ , err = server . loadErrorTemplate ( )
2019-08-13 13:37:01 +01:00
if err != nil {
return Error . Wrap ( err )
}
ctx , cancel := context . WithCancel ( ctx )
var group errgroup . Group
group . Go ( func ( ) error {
<- ctx . Done ( )
2019-08-22 12:40:15 +01:00
return server . server . Shutdown ( context . Background ( ) )
2019-08-13 13:37:01 +01:00
} )
group . Go ( func ( ) error {
2021-08-17 20:38:34 +01:00
server . ipRateLimiter . Run ( ctx )
2020-04-08 20:40:49 +01:00
return nil
} )
group . Go ( func ( ) error {
2019-08-13 13:37:01 +01:00
defer cancel ( )
2020-04-16 16:50:22 +01:00
err := server . server . Serve ( server . listener )
if errs2 . IsCanceled ( err ) || errors . Is ( err , http . ErrServerClosed ) {
err = nil
}
return err
2019-08-13 13:37:01 +01:00
} )
return group . Wait ( )
}
2023-05-17 19:18:54 +01:00
// NewFrontendServer creates new instance of console front-end server.
// NB: The return type is currently consoleweb.Server, but it does not contain all the dependencies.
// It should only be used with RunFrontEnd and Close. We plan on moving this to its own type, but
// right now since we have a feature flag to allow the backend server to continue serving the frontend, it
// makes it easier if they are the same type.
func NewFrontendServer ( logger * zap . Logger , config Config , listener net . Listener , nodeURL storj . NodeURL , stripePublicKey string ) ( server * Server , err error ) {
server = & Server {
log : logger ,
config : config ,
listener : listener ,
nodeURL : nodeURL ,
stripePublicKey : stripePublicKey ,
}
logger . Debug ( "Starting Satellite UI server." , zap . Stringer ( "Address" , server . listener . Addr ( ) ) )
router := mux . NewRouter ( )
// N.B. This middleware has to be the first one because it has to be called
// the earliest in the HTTP chain.
router . Use ( newTraceRequestMiddleware ( logger , router ) )
// in local development, proxy certain requests to the console back-end server
if config . BackendReverseProxy != "" {
target , err := url . Parse ( config . BackendReverseProxy )
if err != nil {
return nil , Error . Wrap ( err )
}
proxy := httputil . NewSingleHostReverseProxy ( target )
logger . Debug ( "Reverse proxy targeting" , zap . String ( "address" , config . BackendReverseProxy ) )
router . PathPrefix ( "/api" ) . Handler ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} ) )
router . PathPrefix ( "/oauth" ) . Handler ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} ) )
router . HandleFunc ( "/.well-known/openid-configuration" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
router . HandleFunc ( "/invited" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
router . HandleFunc ( "/activation" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
router . HandleFunc ( "/cancel-password-recovery" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
router . HandleFunc ( "/registrationToken/" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
router . HandleFunc ( "/robots.txt" , func ( w http . ResponseWriter , r * http . Request ) {
proxy . ServeHTTP ( w , r )
} )
}
fs := http . FileServer ( http . Dir ( server . config . StaticDir ) )
router . HandleFunc ( "/robots.txt" , server . seoHandler )
router . PathPrefix ( "/static/" ) . Handler ( server . brotliMiddleware ( http . StripPrefix ( "/static" , fs ) ) )
router . HandleFunc ( "/config" , server . frontendConfigHandler )
if server . config . UseVuetifyProject {
router . PathPrefix ( "/vuetifypoc" ) . Handler ( http . HandlerFunc ( server . vuetifyAppHandler ) )
}
router . PathPrefix ( "/" ) . Handler ( http . HandlerFunc ( server . appHandler ) )
server . server = http . Server {
Handler : server . withRequest ( router ) ,
MaxHeaderBytes : ContentLengthLimit . Int ( ) ,
}
return server , nil
}
// RunFrontend starts the server that runs the webapp.
func ( server * Server ) RunFrontend ( ctx context . Context ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
ctx , cancel := context . WithCancel ( ctx )
var group errgroup . Group
group . Go ( func ( ) error {
<- ctx . Done ( )
return server . server . Shutdown ( context . Background ( ) )
} )
group . Go ( func ( ) error {
defer cancel ( )
err := server . server . Serve ( server . listener )
if errs2 . IsCanceled ( err ) || errors . Is ( err , http . ErrServerClosed ) {
err = nil
}
return err
} )
return group . Wait ( )
}
2020-07-16 15:18:02 +01:00
// Close closes server and underlying listener.
2019-08-13 13:37:01 +01:00
func ( server * Server ) Close ( ) error {
return server . server . Close ( )
}
2023-06-08 22:53:24 +01:00
// setAppHeaders sets the necessary headers for requests to the app.
func ( server * Server ) setAppHeaders ( w http . ResponseWriter , r * http . Request ) {
2019-07-30 11:13:24 +01:00
header := w . Header ( )
2021-04-09 12:37:33 +01:00
if server . config . CSPEnabled {
cspValues := [ ] string {
"default-src 'self'" ,
2022-05-27 15:01:30 +01:00
"script-src 'sha256-wAqYV6m2PHGd1WDyFBnZmSoyfCK0jxFAns0vGbdiWUA=' 'self' *.stripe.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://hcaptcha.com *.hcaptcha.com" ,
2023-08-31 02:31:10 +01:00
"connect-src 'self' *.tardigradeshare.io *.storjshare.io *.storjapi.io https://hcaptcha.com *.hcaptcha.com " + server . config . GatewayCredentialsRequestURL ,
2021-04-09 12:37:33 +01:00
"frame-ancestors " + server . config . FrameAncestors ,
2022-05-27 15:01:30 +01:00
"frame-src 'self' *.stripe.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ https://hcaptcha.com *.hcaptcha.com" ,
2023-06-12 18:31:00 +01:00
"img-src 'self' data: blob: *.tardigradeshare.io *.storjshare.io *.storjsatelliteshare.io" ,
2021-12-09 15:05:21 +00:00
// Those are hashes of charts custom tooltip inline styles. They have to be updated if styles are updated.
2023-05-16 13:09:46 +01:00
"style-src 'unsafe-hashes' 'sha256-7mY2NKmZ4PuyjGUa4FYC5u36SxXdoUM/zxrlr3BEToo=' 'sha256-PRTMwLUW5ce9tdiUrVCGKqj6wPeuOwGogb1pmyuXhgI=' 'sha256-kwpt3lQZ21rs4cld7/uEm9qI5yAbjYzx+9FGm/XmwNU=' 'sha256-Qf4xqtNKtDLwxce6HLtD5Y6BWpOeR7TnDpNSo+Bhb3s=' 'self' https://hcaptcha.com *.hcaptcha.com" ,
2023-06-12 18:31:00 +01:00
"media-src 'self' blob: *.tardigradeshare.io *.storjshare.io *.storjsatelliteshare.io" ,
2021-04-09 12:37:33 +01:00
}
header . Set ( "Content-Security-Policy" , strings . Join ( cspValues , "; " ) )
2019-07-30 11:13:24 +01:00
}
2019-09-09 19:33:05 +01:00
header . Set ( contentType , "text/html; charset=UTF-8" )
header . Set ( "X-Content-Type-Options" , "nosniff" )
2019-11-21 16:15:22 +00:00
header . Set ( "Referrer-Policy" , "same-origin" ) // Only expose the referring url when navigating around the satellite itself.
2023-06-08 22:53:24 +01:00
}
2019-07-30 11:13:24 +01:00
2023-06-08 22:53:24 +01:00
// appHandler is web app http handler function.
func ( server * Server ) appHandler ( w http . ResponseWriter , r * http . Request ) {
server . setAppHeaders ( w , r )
2023-06-08 14:52:40 +01:00
2023-06-08 22:53:24 +01:00
path := filepath . Join ( server . config . StaticDir , "dist" , "index.html" )
2023-04-05 23:35:01 +01:00
file , err := os . Open ( path )
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
server . log . Error ( "index.html was not generated. run 'npm run build' in the " + server . config . StaticDir + " directory" , zap . Error ( err ) )
} else {
server . log . Error ( "error loading index.html" , zap . String ( "path" , path ) , zap . Error ( err ) )
}
2020-09-16 17:01:09 +01:00
return
}
2023-04-05 23:35:01 +01:00
defer func ( ) {
if err := file . Close ( ) ; err != nil {
server . log . Error ( "error closing index.html" , zap . String ( "path" , path ) , zap . Error ( err ) )
}
} ( )
info , err := file . Stat ( )
if err != nil {
2023-04-17 14:49:53 +01:00
server . log . Error ( "failed to retrieve index.html file info" , zap . Error ( err ) )
2019-08-13 13:37:01 +01:00
return
}
2023-04-05 23:35:01 +01:00
http . ServeContent ( w , r , path , info . ModTime ( ) , file )
2019-01-24 16:26:36 +00:00
}
2023-06-08 22:53:24 +01:00
// vuetifyAppHandler is web app http handler function.
func ( server * Server ) vuetifyAppHandler ( w http . ResponseWriter , r * http . Request ) {
server . setAppHeaders ( w , r )
path := filepath . Join ( server . config . StaticDir , "dist_vuetify_poc" , "index-vuetify.html" )
file , err := os . Open ( path )
if err != nil {
if errors . Is ( err , fs . ErrNotExist ) {
server . log . Error ( "index-vuetify.html was not generated. run 'npm run build-vuetify' in the " + server . config . StaticDir + " directory" , zap . Error ( err ) )
} else {
server . log . Error ( "error loading index-vuetify.html" , zap . String ( "path" , path ) , zap . Error ( err ) )
}
return
}
defer func ( ) {
if err := file . Close ( ) ; err != nil {
server . log . Error ( "error closing index-vuetify.html" , zap . String ( "path" , path ) , zap . Error ( err ) )
}
} ( )
info , err := file . Stat ( )
if err != nil {
server . log . Error ( "failed to retrieve index-vuetify.html file info" , zap . Error ( err ) )
return
}
http . ServeContent ( w , r , path , info . ModTime ( ) , file )
}
2023-06-12 21:25:53 +01:00
// withCORS handles setting CORS-related headers on an http request.
func ( server * Server ) withCORS ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , strings . Trim ( server . config . ExternalAddress , "/" ) )
w . Header ( ) . Set ( "Access-Control-Allow-Credentials" , "true" )
w . Header ( ) . Set ( "Access-Control-Allow-Headers" , "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization" )
w . Header ( ) . Set ( "Access-Control-Expose-Headers" , "*, Authorization" )
if r . Method == http . MethodOptions {
match := & mux . RouteMatch { }
if server . router . Match ( r , match ) {
methods , err := match . Route . GetMethods ( )
if err == nil && len ( methods ) > 0 {
w . Header ( ) . Set ( "Access-Control-Allow-Methods" , strings . Join ( methods , ", " ) )
}
}
return
}
handler . ServeHTTP ( w , r )
} )
}
2022-07-30 04:15:30 +01:00
// withAuth performs initial authorization before every request.
2019-10-21 17:42:49 +01:00
func ( server * Server ) withAuth ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
var err error
2022-07-30 04:15:30 +01:00
ctx := r . Context ( )
2020-01-20 18:57:14 +00:00
2019-10-21 17:42:49 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2022-07-30 04:15:30 +01:00
defer func ( ) {
2020-01-20 18:57:14 +00:00
if err != nil {
2023-06-28 14:06:32 +01:00
web . ServeJSONError ( ctx , server . log , w , http . StatusUnauthorized , console . ErrUnauthorized . Wrap ( err ) )
2022-07-30 04:15:30 +01:00
server . cookieAuth . RemoveTokenCookie ( w )
2020-01-20 18:57:14 +00:00
}
2022-07-30 04:15:30 +01:00
} ( )
2020-01-20 18:57:14 +00:00
2022-07-19 10:26:18 +01:00
tokenInfo , err := server . cookieAuth . GetToken ( r )
2022-07-30 04:15:30 +01:00
if err != nil {
return
2019-10-21 17:42:49 +01:00
}
2022-07-19 10:26:18 +01:00
newCtx , err := server . service . TokenAuth ( ctx , tokenInfo . Token , time . Now ( ) )
2022-07-30 04:15:30 +01:00
if err != nil {
return
}
ctx = newCtx
2020-01-20 18:57:14 +00:00
2019-10-21 17:42:49 +01:00
handler . ServeHTTP ( w , r . Clone ( ctx ) )
} )
}
2020-09-06 02:56:07 +01:00
// withRequest ensures the http request itself is reachable from the context.
func ( server * Server ) withRequest ( handler http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
handler . ServeHTTP ( w , r . Clone ( console . WithRequest ( r . Context ( ) , r ) ) )
} )
}
2023-02-22 10:32:32 +00:00
// frontendConfigHandler handles sending the frontend config to the client.
func ( server * Server ) frontendConfigHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
defer mon . Task ( ) ( & ctx ) ( nil )
w . Header ( ) . Set ( contentType , applicationJSON )
cfg := FrontendConfig {
ExternalAddress : server . config . ExternalAddress ,
SatelliteName : server . config . SatelliteName ,
SatelliteNodeURL : server . nodeURL . String ( ) ,
StripePublicKey : server . stripePublicKey ,
PartneredSatellites : server . config . PartneredSatellites ,
DefaultProjectLimit : server . config . DefaultProjectLimit ,
GeneralRequestURL : server . config . GeneralRequestURL ,
ProjectLimitsIncreaseRequestURL : server . config . ProjectLimitsIncreaseRequestURL ,
GatewayCredentialsRequestURL : server . config . GatewayCredentialsRequestURL ,
IsBetaSatellite : server . config . IsBetaSatellite ,
BetaSatelliteFeedbackURL : server . config . BetaSatelliteFeedbackURL ,
BetaSatelliteSupportURL : server . config . BetaSatelliteSupportURL ,
DocumentationURL : server . config . DocumentationURL ,
CouponCodeBillingUIEnabled : server . config . CouponCodeBillingUIEnabled ,
CouponCodeSignupUIEnabled : server . config . CouponCodeSignupUIEnabled ,
FileBrowserFlowDisabled : server . config . FileBrowserFlowDisabled ,
LinksharingURL : server . config . LinksharingURL ,
2023-05-15 21:18:34 +01:00
PublicLinksharingURL : server . config . PublicLinksharingURL ,
2023-02-22 10:32:32 +00:00
PathwayOverviewEnabled : server . config . PathwayOverviewEnabled ,
DefaultPaidStorageLimit : server . config . UsageLimits . Storage . Paid ,
DefaultPaidBandwidthLimit : server . config . UsageLimits . Bandwidth . Paid ,
Captcha : server . config . Captcha ,
AllProjectsDashboard : server . config . AllProjectsDashboard ,
2023-05-30 22:11:29 +01:00
LimitsAreaEnabled : server . config . LimitsAreaEnabled ,
2023-02-22 10:32:32 +00:00
InactivityTimerEnabled : server . config . Session . InactivityTimerEnabled ,
InactivityTimerDuration : server . config . Session . InactivityTimerDuration ,
InactivityTimerViewerEnabled : server . config . Session . InactivityTimerViewerEnabled ,
OptionalSignupSuccessURL : server . config . OptionalSignupSuccessURL ,
HomepageURL : server . config . HomepageURL ,
NativeTokenPaymentsEnabled : server . config . NativeTokenPaymentsEnabled ,
PasswordMinimumLength : console . PasswordMinimumLength ,
PasswordMaximumLength : console . PasswordMaximumLength ,
ABTestingEnabled : server . config . ABTesting . Enabled ,
2023-05-03 23:12:48 +01:00
PricingPackagesEnabled : server . config . PricingPackagesEnabled ,
2023-05-19 15:24:59 +01:00
NewUploadModalEnabled : server . config . NewUploadModalEnabled ,
2023-06-01 14:20:35 +01:00
GalleryViewEnabled : server . config . GalleryViewEnabled ,
2023-07-10 17:53:39 +01:00
NeededTransactionConfirmations : server . neededTokenPaymentConfirmations ,
2023-08-31 12:33:39 +01:00
ObjectBrowserPaginationEnabled : server . config . ObjectBrowserPaginationEnabled ,
2023-02-22 10:32:32 +00:00
}
err := json . NewEncoder ( w ) . Encode ( & cfg )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
server . log . Error ( "failed to write frontend config" , zap . Error ( err ) )
}
}
2020-01-18 02:34:06 +00:00
// createRegistrationTokenHandler is web app http handler function.
2019-08-08 13:12:39 +01:00
func ( server * Server ) createRegistrationTokenHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-03-19 17:55:43 +00:00
w . Header ( ) . Set ( contentType , applicationJSON )
var response struct {
Secret string ` json:"secret" `
Error string ` json:"error,omitempty" `
}
defer func ( ) {
err := json . NewEncoder ( w ) . Encode ( & response )
if err != nil {
2019-08-08 13:12:39 +01:00
server . log . Error ( err . Error ( ) )
2019-03-19 17:55:43 +00:00
}
} ( )
2020-01-14 13:38:32 +00:00
equality := subtle . ConstantTimeCompare (
[ ] byte ( r . Header . Get ( "Authorization" ) ) ,
[ ] byte ( server . config . AuthToken ) ,
)
if equality != 1 {
2019-03-19 17:55:43 +00:00
w . WriteHeader ( 401 )
response . Error = "unauthorized"
return
}
2019-08-08 13:12:39 +01:00
projectsLimitInput := r . URL . Query ( ) . Get ( "projectsLimit" )
2019-03-19 17:55:43 +00:00
projectsLimit , err := strconv . Atoi ( projectsLimitInput )
if err != nil {
response . Error = err . Error ( )
return
}
2019-08-08 13:12:39 +01:00
token , err := server . service . CreateRegToken ( ctx , projectsLimit )
2019-03-19 17:55:43 +00:00
if err != nil {
response . Error = err . Error ( )
return
}
response . Secret = token . Secret . String ( )
}
2020-07-16 15:18:02 +01:00
// accountActivationHandler is web app http handler function.
2019-08-08 13:12:39 +01:00
func ( server * Server ) accountActivationHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-08-08 13:12:39 +01:00
activationToken := r . URL . Query ( ) . Get ( "token" )
2019-03-08 14:01:11 +00:00
2022-06-05 23:41:38 +01:00
user , err := server . service . ActivateAccount ( ctx , activationToken )
2019-03-08 14:01:11 +00:00
if err != nil {
2022-06-21 12:39:44 +01:00
if console . ErrTokenInvalid . Has ( err ) {
server . log . Debug ( "account activation" ,
zap . String ( "token" , activationToken ) ,
zap . Error ( err ) ,
)
server . serveError ( w , http . StatusBadRequest )
return
}
if console . ErrTokenExpiration . Has ( err ) {
server . log . Debug ( "account activation" ,
zap . String ( "token" , activationToken ) ,
zap . Error ( err ) ,
)
2022-10-11 22:36:29 +01:00
http . Redirect ( w , r , server . config . ExternalAddress + "activate?expired=true" , http . StatusTemporaryRedirect )
2022-06-21 12:39:44 +01:00
return
}
2019-04-09 13:20:29 +01:00
2020-02-10 12:03:38 +00:00
if console . ErrEmailUsed . Has ( err ) {
2022-06-21 12:39:44 +01:00
server . log . Debug ( "account activation" ,
zap . String ( "token" , activationToken ) ,
zap . Error ( err ) ,
)
2021-07-26 17:11:44 +01:00
http . Redirect ( w , r , server . config . ExternalAddress + "login?activated=false" , http . StatusTemporaryRedirect )
2020-02-10 12:03:38 +00:00
return
}
if console . Error . Has ( err ) {
2022-06-21 12:39:44 +01:00
server . log . Error ( "activation: failed to activate account with a valid token" ,
zap . Error ( err ) )
2020-02-10 12:03:38 +00:00
server . serveError ( w , http . StatusInternalServerError )
return
}
2022-06-21 12:39:44 +01:00
server . log . Error (
"activation: failed to activate account with a valid token and unknown error type. BUG: missed error type check" ,
zap . Error ( err ) )
server . serveError ( w , http . StatusInternalServerError )
2019-03-08 14:01:11 +00:00
return
}
2022-06-05 23:41:38 +01:00
ip , err := web . GetRequestIP ( r )
if err != nil {
server . serveError ( w , http . StatusInternalServerError )
return
}
2022-07-19 10:26:18 +01:00
tokenInfo , err := server . service . GenerateSessionToken ( ctx , user . ID , user . Email , ip , r . UserAgent ( ) )
2022-06-05 23:41:38 +01:00
if err != nil {
server . serveError ( w , http . StatusInternalServerError )
return
}
2022-07-19 10:26:18 +01:00
server . cookieAuth . SetTokenCookie ( w , * tokenInfo )
2021-10-06 14:33:54 +01:00
http . Redirect ( w , r , server . config . ExternalAddress , http . StatusTemporaryRedirect )
2019-03-08 14:01:11 +00:00
}
2019-08-08 13:12:39 +01:00
func ( server * Server ) cancelPasswordRecoveryHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( nil )
2019-08-08 13:12:39 +01:00
recoveryToken := r . URL . Query ( ) . Get ( "token" )
2019-05-13 16:53:52 +01:00
// No need to check error as we anyway redirect user to support page
2019-08-08 13:12:39 +01:00
_ = server . service . RevokeResetPasswordToken ( ctx , recoveryToken )
2019-05-13 16:53:52 +01:00
2019-08-13 13:37:01 +01:00
// TODO: Should place this link to config
2019-08-08 13:12:39 +01:00
http . Redirect ( w , r , "https://storjlabs.atlassian.net/servicedesk/customer/portals" , http . StatusSeeOther )
2019-05-13 16:53:52 +01:00
}
2023-06-14 11:09:11 +01:00
func ( server * Server ) handleInvited ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
defer mon . Task ( ) ( & ctx ) ( nil )
token := r . URL . Query ( ) . Get ( "invite" )
if token == "" {
server . serveError ( w , http . StatusBadRequest )
return
}
loginLink := server . config . ExternalAddress + "login"
invite , err := server . service . GetInviteByToken ( ctx , token )
if err != nil {
server . log . Error ( "handleInvited: error checking invitation" , zap . Error ( err ) )
if console . ErrProjectInviteInvalid . Has ( err ) {
http . Redirect ( w , r , loginLink + "?invite_invalid=true" , http . StatusTemporaryRedirect )
return
}
server . serveError ( w , http . StatusInternalServerError )
return
}
2023-06-26 17:53:46 +01:00
user , _ , err := server . service . GetUserByEmailWithUnverified ( ctx , invite . Email )
if err != nil && ! console . ErrEmailNotFound . Has ( err ) {
server . log . Error ( "error getting invitation recipient" , zap . Error ( err ) )
server . serveError ( w , http . StatusInternalServerError )
return
}
if user != nil {
2023-06-30 10:11:11 +01:00
http . Redirect ( w , r , loginLink + "?email=" + url . QueryEscape ( user . Email ) , http . StatusTemporaryRedirect )
2023-06-26 17:53:46 +01:00
return
}
params := url . Values { "email" : { strings . ToLower ( invite . Email ) } }
if invite . InviterID != nil {
inviter , err := server . service . GetUser ( ctx , * invite . InviterID )
if err != nil {
server . log . Error ( "error getting invitation sender" , zap . Error ( err ) )
server . serveError ( w , http . StatusInternalServerError )
return
}
name := inviter . ShortName
if name == "" {
name = inviter . FullName
}
params . Add ( "inviter" , name )
params . Add ( "inviter_email" , inviter . Email )
}
proj , err := server . service . GetProjectNoAuth ( ctx , invite . ProjectID )
if err != nil {
server . log . Error ( "error getting invitation project" , zap . Error ( err ) )
server . serveError ( w , http . StatusInternalServerError )
return
}
params . Add ( "project" , proj . Name )
http . Redirect ( w , r , server . config . ExternalAddress + "signup?" + params . Encode ( ) , http . StatusTemporaryRedirect )
2023-06-14 11:09:11 +01:00
}
2023-09-07 20:11:20 +01:00
// graphqlHandler is graphql endpoint http handler function.
func ( server * Server ) graphqlHandler ( w http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
defer mon . Task ( ) ( & ctx ) ( nil )
handleError := func ( code int , err error ) {
w . WriteHeader ( code )
var jsonError struct {
Error string ` json:"error" `
RequestID string ` json:"requestID" `
}
jsonError . Error = err . Error ( )
if requestID := requestid . FromContext ( ctx ) ; requestID != "" {
jsonError . RequestID = requestID
}
if err := json . NewEncoder ( w ) . Encode ( jsonError ) ; err != nil {
server . log . Error ( "error graphql error" , zap . Error ( err ) )
}
}
w . Header ( ) . Set ( contentType , applicationJSON )
query , err := getQuery ( w , r )
if err != nil {
handleError ( http . StatusBadRequest , err )
return
}
rootObject := make ( map [ string ] interface { } )
rootObject [ "origin" ] = server . config . ExternalAddress
rootObject [ consoleql . ActivationPath ] = "activation?token="
rootObject [ consoleql . PasswordRecoveryPath ] = "password-recovery?token="
rootObject [ consoleql . CancelPasswordRecoveryPath ] = "cancel-password-recovery?token="
rootObject [ consoleql . SignInPath ] = "login"
rootObject [ consoleql . LetUsKnowURL ] = server . config . LetUsKnowURL
rootObject [ consoleql . ContactInfoURL ] = server . config . ContactInfoURL
rootObject [ consoleql . TermsAndConditionsURL ] = server . config . TermsAndConditionsURL
rootObject [ consoleql . SatelliteRegion ] = server . config . SatelliteName
result := graphql . Do ( graphql . Params {
Schema : server . schema ,
Context : ctx ,
RequestString : query . Query ,
VariableValues : query . Variables ,
OperationName : query . OperationName ,
RootObject : rootObject ,
} )
getGqlError := func ( err gqlerrors . FormattedError ) error {
var gerr * gqlerrors . Error
if errors . As ( err . OriginalError ( ) , & gerr ) {
return gerr . OriginalError
}
return nil
}
parseConsoleError := func ( err error ) ( int , error ) {
switch {
case console . ErrUnauthorized . Has ( err ) :
return http . StatusUnauthorized , err
case console . Error . Has ( err ) :
return http . StatusInternalServerError , err
}
return 0 , nil
}
handleErrors := func ( code int , errors gqlerrors . FormattedErrors ) {
w . WriteHeader ( code )
var jsonError struct {
Errors [ ] string ` json:"errors" `
RequestID string ` json:"requestID" `
}
for _ , err := range errors {
jsonError . Errors = append ( jsonError . Errors , err . Message )
}
if requestID := requestid . FromContext ( ctx ) ; requestID != "" {
jsonError . RequestID = requestID
}
if err := json . NewEncoder ( w ) . Encode ( jsonError ) ; err != nil {
server . log . Error ( "error graphql error" , zap . Error ( err ) )
}
}
handleGraphqlErrors := func ( ) {
for _ , err := range result . Errors {
gqlErr := getGqlError ( err )
if gqlErr == nil {
continue
}
code , err := parseConsoleError ( gqlErr )
if err != nil {
handleError ( code , err )
return
}
}
handleErrors ( http . StatusOK , result . Errors )
}
if result . HasErrors ( ) {
handleGraphqlErrors ( )
return
}
err = json . NewEncoder ( w ) . Encode ( result )
if err != nil {
server . log . Error ( "error encoding grapql result" , zap . Error ( err ) )
return
}
server . log . Debug ( fmt . Sprintf ( "%s" , result ) )
}
2023-04-05 23:35:01 +01:00
// serveError serves a static error page.
2019-12-12 12:58:15 +00:00
func ( server * Server ) serveError ( w http . ResponseWriter , status int ) {
w . WriteHeader ( status )
2023-04-05 23:35:01 +01:00
template , err := server . loadErrorTemplate ( )
2023-04-13 13:37:34 +01:00
if err != nil {
2023-04-05 23:35:01 +01:00
server . log . Error ( "unable to load error template" , zap . Error ( err ) )
2023-04-13 13:37:34 +01:00
return
}
data := struct { StatusCode int } { StatusCode : status }
2023-04-05 23:35:01 +01:00
err = template . Execute ( w , data )
2023-04-13 13:37:34 +01:00
if err != nil {
server . log . Error ( "cannot parse error template" , zap . Error ( err ) )
2019-12-12 12:58:15 +00:00
}
}
2020-07-16 15:18:02 +01:00
// seoHandler used to communicate with web crawlers and other web robots.
2019-09-09 19:33:05 +01:00
func ( server * Server ) seoHandler ( w http . ResponseWriter , req * http . Request ) {
header := w . Header ( )
2023-09-07 14:48:36 +01:00
header . Set ( contentType , typeByExtension ( ".txt" ) )
2019-09-09 19:33:05 +01:00
header . Set ( "X-Content-Type-Options" , "nosniff" )
2019-09-27 17:48:53 +01:00
_ , err := w . Write ( [ ] byte ( server . config . SEO ) )
2019-09-09 19:33:05 +01:00
if err != nil {
server . log . Error ( err . Error ( ) )
}
}
2020-12-15 19:06:26 +00:00
// brotliMiddleware is used to compress static content using brotli to minify resources if browser support such decoding.
func ( server * Server ) brotliMiddleware ( fn http . Handler ) http . Handler {
2019-08-08 13:12:39 +01:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2019-11-12 13:05:35 +00:00
w . Header ( ) . Set ( "Cache-Control" , "public, max-age=31536000" )
w . Header ( ) . Set ( "X-Content-Type-Options" , "nosniff" )
2020-12-15 19:06:26 +00:00
isBrotliSupported := strings . Contains ( r . Header . Get ( "Accept-Encoding" ) , "br" )
if ! isBrotliSupported {
2019-11-12 13:05:35 +00:00
fn . ServeHTTP ( w , r )
return
2019-08-08 13:12:39 +01:00
}
2019-08-13 13:37:01 +01:00
2020-12-15 19:06:26 +00:00
info , err := os . Stat ( server . config . StaticDir + strings . TrimPrefix ( r . URL . Path , "/static" ) + ".br" )
2019-11-12 13:05:35 +00:00
if err != nil {
2019-08-08 13:12:39 +01:00
fn . ServeHTTP ( w , r )
return
}
2019-11-12 13:05:35 +00:00
extension := filepath . Ext ( info . Name ( ) [ : len ( info . Name ( ) ) - 3 ] )
2023-09-07 14:48:36 +01:00
w . Header ( ) . Set ( contentType , typeByExtension ( extension ) )
2020-12-15 19:06:26 +00:00
w . Header ( ) . Set ( "Content-Encoding" , "br" )
2019-08-08 13:12:39 +01:00
newRequest := new ( http . Request )
* newRequest = * r
newRequest . URL = new ( url . URL )
* newRequest . URL = * r . URL
2020-12-15 19:06:26 +00:00
newRequest . URL . Path += ".br"
2019-08-08 13:12:39 +01:00
fn . ServeHTTP ( w , newRequest )
} )
2019-01-24 16:26:36 +00:00
}
2019-08-13 13:37:01 +01:00
2023-04-18 11:42:17 +01:00
//go:embed error_fallback.html
var errorTemplateFallback string
2023-04-05 23:35:01 +01:00
// loadTemplates is used to initialize the error page template.
func ( server * Server ) loadErrorTemplate ( ) ( _ * template . Template , err error ) {
if server . errorTemplate == nil || server . config . Watch {
server . errorTemplate , err = template . ParseFiles ( filepath . Join ( server . config . StaticDir , "static" , "errors" , "error.html" ) )
if err != nil {
2023-04-18 11:42:17 +01:00
server . log . Error ( "failed to load error.html template, falling back to error_fallback.html" , zap . Error ( err ) )
server . errorTemplate , err = template . New ( "" ) . Parse ( errorTemplateFallback )
if err != nil {
return nil , Error . Wrap ( err )
}
2023-04-05 23:35:01 +01:00
}
2019-08-13 13:37:01 +01:00
}
2023-04-05 23:35:01 +01:00
return server . errorTemplate , nil
2019-08-13 13:37:01 +01:00
}
2021-08-17 20:38:34 +01:00
// NewUserIDRateLimiter constructs a RateLimiter that limits based on user ID.
2022-11-21 18:58:42 +00:00
func NewUserIDRateLimiter ( config web . RateLimiterConfig , log * zap . Logger ) * web . RateLimiter {
return web . NewRateLimiter ( config , log , func ( r * http . Request ) ( string , error ) {
2022-06-05 23:41:38 +01:00
user , err := console . GetUser ( r . Context ( ) )
2021-08-17 20:38:34 +01:00
if err != nil {
return "" , err
}
2022-06-05 23:41:38 +01:00
return user . ID . String ( ) , nil
2021-08-17 20:38:34 +01:00
} )
}
2022-06-30 18:44:17 +01:00
// responseWriterStatusCode is a wrapper of an http.ResponseWriter to track the
// response status code for having access to it after calling
// http.ResponseWriter.WriteHeader.
type responseWriterStatusCode struct {
http . ResponseWriter
code int
}
func ( w * responseWriterStatusCode ) WriteHeader ( code int ) {
w . code = code
w . ResponseWriter . WriteHeader ( code )
}
// newTraceRequestMiddleware returns middleware for tracing each request to a
// registered endpoint through Monkit.
//
2022-08-04 19:00:44 +01:00
// It also log in INFO level each request.
2022-06-30 18:44:17 +01:00
func newTraceRequestMiddleware ( log * zap . Logger , root * mux . Router ) mux . MiddlewareFunc {
log = log . Named ( "trace-request-middleware" )
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
begin := time . Now ( )
ctx := r . Context ( )
respWCode := responseWriterStatusCode { ResponseWriter : w , code : 0 }
defer func ( ) {
// Preallocate the maximum fields that we are going to use for avoiding
// reallocations
fields := make ( [ ] zapcore . Field , 0 , 6 )
fields = append ( fields ,
zap . String ( "method" , r . Method ) ,
zap . String ( "URI" , r . RequestURI ) ,
zap . String ( "IP" , getClientIP ( r ) ) ,
zap . Int ( "response-code" , respWCode . code ) ,
zap . Duration ( "elapse" , time . Since ( begin ) ) ,
)
span := monkit . SpanFromCtx ( ctx )
if span != nil {
fields = append ( fields , zap . Int64 ( "trace-id" , span . Trace ( ) . Id ( ) ) )
}
2022-08-04 19:00:44 +01:00
log . Info ( "client HTTP request" , fields ... )
2022-06-30 18:44:17 +01:00
} ( )
match := mux . RouteMatch { }
root . Match ( r , & match )
pathTpl , err := match . Route . GetPathTemplate ( )
if err != nil {
log . Warn ( "error when getting the route template path" ,
zap . Error ( err ) , zap . String ( "request-uri" , r . RequestURI ) ,
)
next . ServeHTTP ( & respWCode , r )
return
}
// Limit the values accepted as an HTTP method for avoiding to create an
// unbounded amount of metrics.
boundMethod := r . Method
switch r . Method {
case http . MethodDelete :
case http . MethodGet :
case http . MethodHead :
case http . MethodOptions :
case http . MethodPatch :
case http . MethodPost :
case http . MethodPut :
default :
boundMethod = "INVALID"
}
2022-08-18 16:46:31 +01:00
stop := mon . TaskNamed ( "visit_task" , monkit . NewSeriesTag ( "path" , pathTpl ) , monkit . NewSeriesTag ( "method" , boundMethod ) ) ( & ctx )
2022-06-30 18:44:17 +01:00
r = r . WithContext ( ctx )
defer func ( ) {
var err error
if respWCode . code >= http . StatusBadRequest {
err = fmt . Errorf ( "%d" , respWCode . code )
}
stop ( & err )
// Count the status codes returned by each endpoint.
2022-08-18 16:46:31 +01:00
mon . Event ( "visit_event_by_code" ,
monkit . NewSeriesTag ( "path" , pathTpl ) ,
2022-06-30 18:44:17 +01:00
monkit . NewSeriesTag ( "method" , boundMethod ) ,
monkit . NewSeriesTag ( "code" , strconv . Itoa ( respWCode . code ) ) ,
)
} ( )
// Count the requests to each endpoint.
2022-08-18 16:46:31 +01:00
mon . Event ( "visit_event" , monkit . NewSeriesTag ( "path" , pathTpl ) , monkit . NewSeriesTag ( "method" , boundMethod ) )
2022-06-30 18:44:17 +01:00
next . ServeHTTP ( & respWCode , r )
} )
}
}
2023-05-18 11:07:36 +01:00
// newBodyLimiterMiddleware returns a middleware that places a length limit on each request's body.
func newBodyLimiterMiddleware ( log * zap . Logger , limit memory . Size ) mux . MiddlewareFunc {
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if r . ContentLength > limit . Int64 ( ) {
2023-06-28 14:06:32 +01:00
web . ServeJSONError ( r . Context ( ) , log , w , http . StatusRequestEntityTooLarge , errs . New ( "Request body is too large" ) )
2023-05-18 11:07:36 +01:00
return
}
r . Body = http . MaxBytesReader ( w , r . Body , limit . Int64 ( ) )
next . ServeHTTP ( w , r )
} )
}
}