Logs
Patchsets
Range Diff ↕
1: 77aaa29 ! 1: 8d56535 reactor(metric-drain): use caddy json format
@@ auth/api.go "log/slog" "net/http" "net/url" + "strings" "time" "github.com/gorilla/feeds" } } +type AccessLogReq struct { + RemoteIP string `json:"remote_ip"` + RemotePort string `json:"remote_port"` + ClientIP string `json:"client_ip"` + Method string `json:"method"` + Host string `json:"host"` + Uri string `json:"uri"` + Headers struct { + UserAgent string `json:"User-Agent"` + Referer string `json:"Referer"` + } `json:"headers"` + Tls struct { + ServerName string `json:"server_name"` + } `json:"tls"` +} + +type CaddyAccessLog struct { + Request AccessLogReq `json:"request"` + Status int `json:"status"` +} + +func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) { + spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2) + space := spaceRaw[0] + host := access.Request.Host + path := access.Request.Uri + subdomain := "" + + // grab subdomain based on host + if strings.HasSuffix(host, "tuns.sh") { + subdomain = strings.TrimSuffix(host, ".tuns.sh") + } else if strings.HasSuffix(host, "pgs.sh") { + subdomain = strings.TrimSuffix(host, ".pgs.sh") + } else if strings.HasSuffix(host, "prose.sh") { + subdomain = strings.TrimSuffix(host, ".prose.sh") + } else { + subdomain = shared.GetCustomDomain(host, space) + } + + // get user and namespace details from subdomain + props, err := shared.GetProjectFromSubdomain(subdomain) + if err != nil { + return nil, err + } + // get user ID + user, err := dbpool.FindUserForName(props.Username) + if err != nil { + return nil, err + } + + projectID := "" + postID := "" + if space == "pgs" { // figure out project ID + project, err := dbpool.FindProjectByName(user.ID, props.ProjectName) + if err != nil { + return nil, err + } + projectID = project.ID + } else if space == "prose" { // figure out post ID + if path == "" || path == "/" { + } else { + post, err := dbpool.FindPostWithSlug(path, user.ID, space) + if err != nil { + return nil, err + } + postID = post.ID + } + } + + return &db.AnalyticsVisits{ + UserID: user.ID, + ProjectID: projectID, + PostID: postID, + Namespace: space, + Host: host, + Path: path, + IpAddress: access.Request.ClientIP, + UserAgent: access.Request.Headers.UserAgent, + Referer: access.Request.Headers.Referer, // TODO: I don't see referer in the access log + Status: access.Status, + }, nil +} + func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) { conn := shared.NewPicoPipeClient() stdoutPipe, err := pubsub.RemoteSub("sub metric-drain -k", ctx, conn) scanner := bufio.NewScanner(stdoutPipe) for scanner.Scan() { line := scanner.Text() - visit := db.AnalyticsVisits{} - err := json.Unmarshal([]byte(line), &visit) drain := metrics.ReconnectReadMetrics( ctx, -1, ) - for { - scanner := bufio.NewScanner(drain) - for scanner.Scan() { - line := scanner.Text() - visit := db.AnalyticsVisits{} - err := json.Unmarshal([]byte(line), &visit) - if err != nil { - logger.Error("json unmarshal", "err", err) - continue - } - - user := slog.Any("userId", visit.UserID) - - err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) - if err != nil { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Info("could not record analytics visit", "reason", err, "visit", visit, user) - continue - } - } + scanner := bufio.NewScanner(drain) + for scanner.Scan() { + line := scanner.Text() + accessLog := CaddyAccessLog{} + err := json.Unmarshal([]byte(line), &accessLog) if err != nil { logger.Error("json unmarshal", "err", err) continue } + if err != nil { + logger.Error("json unmarshal", "err", err) + continue + } - err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) - logger.Info("inserting visit", "visit", visit, user) - err = dbpool.InsertVisit(&visit) - if err != nil { - logger.Error("could not insert visit record", "err", err, "visit", visit, user) + visit, err := deserializeCaddyAccessLog(dbpool, &accessLog) + if err != nil { + logger.Error("cannot deserialize access log", "err", err) + continue + } + err = shared.AnalyticsVisitFromVisit(visit, dbpool, secret) if err != nil { if !errors.Is(err, shared.ErrAnalyticsDisabled) { logger.Info("could not record analytics visit", "reason", err) + if err != nil { + if !errors.Is(err, shared.ErrAnalyticsDisabled) { + logger.Info("could not record analytics visit", "reason", err) } } logger.Info("inserting visit", "visit", visit) - err = dbpool.InsertVisit(&visit) - if scanner.Err() != nil { - logger.Error("scanner error", "err", scanner.Err()) + logger.Info("inserting visit", "visit", visit) + err = dbpool.InsertVisit(visit) if err != nil { logger.Error("could not insert visit record", "err", err) + if err != nil { + logger.Error("could not insert visit record", "err", err) } } } @@ caddy.json +{ + "level": "info", + "ts": 1731644477.313701, + "logger": "http.log.access", + "msg": "handled request", + "request": { + "remote_ip": "127.0.0.1", + "remote_port": "40400", + "client_ip": "127.0.0.1", + "proto": "HTTP/2.0", + "method": "GET", + "host": "pgs.sh", + "uri": "/", + "headers": { "User-Agent": ["Blackbox Exporter/0.24.0"] }, + "tls": { + "resumed": false, + "version": 772, + "cipher_suite": 4865, + "proto": "h2", + "server_name": "pgs.sh" + } + }, + "bytes_read": 0, + "user_id": "", + "duration": 0.001207084, + "size": 3718, + "status": 200, + "resp_headers": { + "Referrer-Policy": ["no-referrer-when-downgrade"], + "Strict-Transport-Security": ["max-age=31536000;"], + "X-Content-Type-Options": ["nosniff"], + "X-Frame-Options": ["DENY"], + "Server": ["Caddy"], + "Alt-Svc": ["h3=\":443\"; ma=2592000"], + "Date": ["Fri, 15 Nov 2024 04:21:17 GMT"], + "Content-Type": ["text/html; charset=utf-8"], + "X-Xss-Protection": ["1; mode=block"], + "Permissions-Policy": ["interest-cohort=()"] + } +} @@ pgs/tunnel.go "pubkey", pubkeyStr, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { log.Error(err.Error()) return http.HandlerFunc(shared.UnauthorizedHandler) @@ pgs/web.go if !strings.Contains(hostDomain, appDomain) { subdomain := shared.GetCustomDomain(hostDomain, cfg.Space) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Error( "could not get project from subdomain", "host", r.Host, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Info( "could not determine project from subdomain", ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain) router.ServeHTTP(w, r.WithContext(ctx)) } - -type SubdomainProps struct { - ProjectName string - Username string -} - -func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { - props := &SubdomainProps{} - strs := strings.SplitN(subdomain, "-", 2) - props.Username = strs[0] - if len(strs) == 2 { - props.ProjectName = strs[1] - } else { - props.ProjectName = props.Username - } - return props, nil -} @@ shared/api.go "github.com/picosh/utils" ) +type SubdomainProps struct { + ProjectName string + Username string +} + +func GetProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { + props := &SubdomainProps{} + strs := strings.SplitN(subdomain, "-", 2) + props.Username = strs[0] + if len(strs) == 2 { + props.ProjectName = strs[1] + } else { + props.ProjectName = props.Username + } + return props, nil +} + func CorsHeaders(headers http.Header) { headers.Add("Access-Control-Allow-Origin", "*") headers.Add("Vary", "Origin")
-: ------- > 2: a336041 wip
-: ------- > 3: 7ae45b3 chore: wrap
-: ------- > 4: bfa5c4f done
Range Diff ↕
2: a336041 < -: ------- wip
3: 7ae45b3 < -: ------- chore: wrap
4: bfa5c4f < -: ------- done
1: 8d56535 ! 1: c7eeb12 reactor(metric-drain): use caddy access logs
@@ auth/api.go "log/slog" "net/http" "net/url" + "strings" "time" "github.com/gorilla/feeds" "github.com/picosh/pico/db/postgres" "github.com/picosh/pico/shared" "github.com/picosh/utils" + "github.com/picosh/utils/pipe" "github.com/picosh/utils/pipe/metrics" ) } } +type AccessLogReq struct { + RemoteIP string `json:"remote_ip"` + RemotePort string `json:"remote_port"` + ClientIP string `json:"client_ip"` + Method string `json:"method"` + Host string `json:"host"` + Uri string `json:"uri"` + Headers struct { + UserAgent string `json:"User-Agent"` + Referer string `json:"Referer"` + UserAgent []string `json:"User-Agent"` + Referer []string `json:"Referer"` + } `json:"headers"` + Tls struct { + ServerName string `json:"server_name"` + } `json:"tls"` +} + +type RespHeaders struct { + ContentType []string `json:"Content-Type"` +} + +type CaddyAccessLog struct { + Request AccessLogReq `json:"request"` + Status int `json:"status"` + Request AccessLogReq `json:"request"` + Status int `json:"status"` + RespHeaders RespHeaders `json:"resp_headers"` +} + +func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) { + spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2) + space := spaceRaw[0] + host := access.Request.Host + path := access.Request.Uri + subdomain := "" + + // grab subdomain based on host + if strings.HasSuffix(host, "tuns.sh") { + subdomain = strings.TrimSuffix(host, ".tuns.sh") + } else if strings.HasSuffix(host, "pgs.sh") { + subdomain = strings.TrimSuffix(host, ".pgs.sh") + } else if strings.HasSuffix(host, "prose.sh") { + subdomain = strings.TrimSuffix(host, ".prose.sh") + } else { + subdomain = shared.GetCustomDomain(host, space) + } + + // get user and namespace details from subdomain + props, err := shared.GetProjectFromSubdomain(subdomain) + if err != nil { + return nil, err + } + // get user ID + user, err := dbpool.FindUserForName(props.Username) + if err != nil { + return nil, err + } + + projectID := "" + postID := "" + if space == "pgs" { // figure out project ID + project, err := dbpool.FindProjectByName(user.ID, props.ProjectName) + if err != nil { + return nil, err + } + projectID = project.ID + } else if space == "prose" { // figure out post ID + if path == "" || path == "/" { + } else { + post, err := dbpool.FindPostWithSlug(path, user.ID, space) + if err != nil { + return nil, err + } + postID = post.ID + } + } + + return &db.AnalyticsVisits{ + UserID: user.ID, + ProjectID: projectID, + PostID: postID, + Namespace: space, + Host: host, + Path: path, + IpAddress: access.Request.ClientIP, + UserAgent: access.Request.Headers.UserAgent, + Referer: access.Request.Headers.Referer, // TODO: I don't see referer in the access log + Status: access.Status, + UserID: user.ID, + ProjectID: projectID, + PostID: postID, + Namespace: space, + Host: host, + Path: path, + IpAddress: access.Request.ClientIP, + UserAgent: strings.Join(access.Request.Headers.UserAgent, " "), + Referer: strings.Join(access.Request.Headers.Referer, " "), + ContentType: strings.Join(access.RespHeaders.ContentType, " "), + Status: access.Status, + }, nil +} + +// this feels really stupid because i'm taking containter-drain, +// filtering it, and then sending it to metric-drain. The +// metricDrainSub function listens on the metric-drain and saves it. +// So why not just call the necessary functions to save the visit? +// We want to be able to use pipe as a debugging tool which means we +// can manually sub to `metric-drain` and have a nice clean output to view. +func containerDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger) { + info := shared.NewPicoPipeClient() + drain := pipe.NewReconnectReadWriteCloser( + ctx, + logger, + info, + "container drain", + "sub container-drain -k", + 100, + -1, + ) + + send := pipe.NewReconnectReadWriteCloser( + ctx, + logger, + info, + "from container drain to metric drain", + "pub metric-drain -b=false", + 100, + -1, + ) + + for { + scanner := bufio.NewScanner(drain) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "http.log.access") { + clean := strings.TrimSpace(line) + visit, err := accessLogToVisit(dbpool, clean) + if err != nil { + logger.Debug("could not convert access log to a visit", "err", err) + continue + } + jso, err := json.Marshal(visit) + if err != nil { + logger.Error("could not marshal json of a visit", "err", err) + continue + } + _, _ = send.Write(jso) + } + } + } +} + +func accessLogToVisit(dbpool db.DB, line string) (*db.AnalyticsVisits, error) { + accessLog := CaddyAccessLog{} + err := json.Unmarshal([]byte(line), &accessLog) + if err != nil { + return nil, err + } + + return deserializeCaddyAccessLog(dbpool, &accessLog) +} + func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) { drain := metrics.ReconnectReadMetrics( ctx, -1, ) - for { - scanner := bufio.NewScanner(drain) - for scanner.Scan() { - line := scanner.Text() - visit := db.AnalyticsVisits{} - err := json.Unmarshal([]byte(line), &visit) - if err != nil { visit := db.AnalyticsVisits{} err := json.Unmarshal([]byte(line), &visit) if err != nil { - logger.Error("json unmarshal", "err", err) - continue - } + logger.Info("could not unmarshal json", "err", err, "line", line) continue } - - user := slog.Any("userId", visit.UserID) - - err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) - if err != nil { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) if err != nil { if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Info("could not record analytics visit", "reason", err, "visit", visit, user) - continue - } - } + scanner := bufio.NewScanner(drain) + for scanner.Scan() { + line := scanner.Text() + accessLog := CaddyAccessLog{} + err := json.Unmarshal([]byte(line), &accessLog) + if err != nil { + logger.Error("json unmarshal", "err", err) + continue + } + logger.Info("could not record analytics visit", "reason", err) } } - logger.Info("inserting visit", "visit", visit, user) - err = dbpool.InsertVisit(&visit) - if err != nil { + if visit.ContentType != "" && !strings.HasPrefix(visit.ContentType, "text/html") { + continue + } + + logger.Info("inserting visit", "visit", visit) err = dbpool.InsertVisit(&visit) if err != nil { - logger.Error("could not insert visit record", "err", err, "visit", visit, user) + visit, err := deserializeCaddyAccessLog(dbpool, &accessLog) + if err != nil { + logger.Error("cannot deserialize access log", "err", err) + continue + } + err = shared.AnalyticsVisitFromVisit(visit, dbpool, secret) + if err != nil { + if !errors.Is(err, shared.ErrAnalyticsDisabled) { + logger.Info("could not record analytics visit", "reason", err) + logger.Error("could not insert visit record", "err", err) } } - - if scanner.Err() != nil { - logger.Error("scanner error", "err", scanner.Err()) + logger.Info("inserting visit", "visit", visit) + err = dbpool.InsertVisit(visit) + if err != nil { + logger.Error("could not insert visit record", "err", err) } - } } } // gather metrics in the auth service go metricDrainSub(ctx, db, logger, cfg.Secret) + // convert container logs to access logs + go containerDrainSub(ctx, db, logger) + defer ctx.Done() apiConfig := &shared.ApiConfig{ @@ caddy.json +{ + "level": "info", + "ts": 1731644477.313701, + "logger": "http.log.access", + "msg": "handled request", + "request": { + "remote_ip": "127.0.0.1", + "remote_port": "40400", + "client_ip": "127.0.0.1", + "proto": "HTTP/2.0", + "method": "GET", + "host": "pgs.sh", + "uri": "/", + "headers": { "User-Agent": ["Blackbox Exporter/0.24.0"] }, + "tls": { + "resumed": false, + "version": 772, + "cipher_suite": 4865, + "proto": "h2", + "server_name": "pgs.sh" + } + }, + "bytes_read": 0, + "user_id": "", + "duration": 0.001207084, + "size": 3718, + "status": 200, + "resp_headers": { + "Referrer-Policy": ["no-referrer-when-downgrade"], + "Strict-Transport-Security": ["max-age=31536000;"], + "X-Content-Type-Options": ["nosniff"], + "X-Frame-Options": ["DENY"], + "Server": ["Caddy"], + "Alt-Svc": ["h3=\":443\"; ma=2592000"], + "Date": ["Fri, 15 Nov 2024 04:21:17 GMT"], + "Content-Type": ["text/html; charset=utf-8"], + "X-Xss-Protection": ["1; mode=block"], + "Permissions-Policy": ["interest-cohort=()"] + } +} @@ pgs/tunnel.go "pubkey", pubkeyStr, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { log.Error(err.Error()) return http.HandlerFunc(shared.UnauthorizedHandler) logger, apiConfig.Dbpool, apiConfig.Storage, - apiConfig.AnalyticsQueue, ) tunnelRouter := TunnelWebRouter{routes} router := http.NewServeMux() @@ pgs/web.go return } - ch := make(chan *db.AnalyticsVisits, 100) - go shared.AnalyticsCollect(ch, dbpool, logger) - - routes := NewWebRouter(cfg, logger, dbpool, st, ch) + routes := NewWebRouter(cfg, logger, dbpool, st) portStr := fmt.Sprintf(":%s", cfg.Port) logger.Info( type HasPerm = func(proj *db.Project) bool type WebRouter struct { - Cfg *shared.ConfigSite - Logger *slog.Logger - Dbpool db.DB - Storage storage.StorageServe - AnalyticsQueue chan *db.AnalyticsVisits - RootRouter *http.ServeMux - UserRouter *http.ServeMux + Cfg *shared.ConfigSite + Logger *slog.Logger + Dbpool db.DB + Storage storage.StorageServe + RootRouter *http.ServeMux + UserRouter *http.ServeMux } -func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe, analytics chan *db.AnalyticsVisits) *WebRouter { +func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe) *WebRouter { router := &WebRouter{ - Cfg: cfg, - Logger: logger, - Dbpool: dbpool, - Storage: st, - AnalyticsQueue: analytics, + Cfg: cfg, + Logger: logger, + Dbpool: dbpool, + Storage: st, } router.initRouters() return router if !strings.Contains(hostDomain, appDomain) { subdomain := shared.GetCustomDomain(hostDomain, cfg.Space) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Error( "could not get project from subdomain", "host", r.Host, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Info( "could not determine project from subdomain", ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain) router.ServeHTTP(w, r.WithContext(ctx)) } - -type SubdomainProps struct { - ProjectName string - Username string -} - -func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { - props := &SubdomainProps{} - strs := strings.SplitN(subdomain, "-", 2) - props.Username = strs[0] - if len(strs) == 2 { - props.ProjectName = strs[1] - } else { - props.ProjectName = props.Username - } - return props, nil -} @@ shared/api.go "github.com/picosh/utils" ) +type SubdomainProps struct { + ProjectName string + Username string +} + +func GetProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { + props := &SubdomainProps{} + strs := strings.SplitN(subdomain, "-", 2) + props.Username = strs[0] + if len(strs) == 2 { + props.ProjectName = strs[1] + } else { + props.ProjectName = props.Username + } + return props, nil +} + func CorsHeaders(headers http.Header) { headers.Add("Access-Control-Allow-Origin", "*") headers.Add("Vary", "Origin")
Range Diff ↕
1: c7eeb12 ! 1: 4e0839a reactor(metric-drain): use caddy access logs
@@ Makefile $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20240819_add_projects_blocked.sql $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241028_add_analytics_indexes.sql $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql + $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql .PHONY: migrate latest: - $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241114_add_namespace_to_analytics.sql + $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20241125_add_content_type_to_analytics.sql .PHONY: latest psql: @@ auth/api.go "log/slog" "net/http" "net/url" + "strings" "time" "github.com/gorilla/feeds" "github.com/picosh/pico/db/postgres" "github.com/picosh/pico/shared" "github.com/picosh/utils" + "github.com/picosh/utils/pipe" "github.com/picosh/utils/pipe/metrics" ) } } +type AccessLogReq struct { + RemoteIP string `json:"remote_ip"` + RemotePort string `json:"remote_port"` + ClientIP string `json:"client_ip"` + Method string `json:"method"` + Host string `json:"host"` + Uri string `json:"uri"` + Headers struct { + UserAgent []string `json:"User-Agent"` + Referer []string `json:"Referer"` + } `json:"headers"` + Tls struct { + ServerName string `json:"server_name"` + } `json:"tls"` +} + +type RespHeaders struct { + ContentType []string `json:"Content-Type"` +} + +type CaddyAccessLog struct { + Request AccessLogReq `json:"request"` + Status int `json:"status"` + RespHeaders RespHeaders `json:"resp_headers"` +} + +func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) { + spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2) + space := spaceRaw[0] + host := access.Request.Host + path := access.Request.Uri + subdomain := "" + + // grab subdomain based on host + if strings.HasSuffix(host, "tuns.sh") { + subdomain = strings.TrimSuffix(host, ".tuns.sh") + } else if strings.HasSuffix(host, "pgs.sh") { + subdomain = strings.TrimSuffix(host, ".pgs.sh") + } else if strings.HasSuffix(host, "prose.sh") { + subdomain = strings.TrimSuffix(host, ".prose.sh") + } else { + subdomain = shared.GetCustomDomain(host, space) + } + + // get user and namespace details from subdomain + props, err := shared.GetProjectFromSubdomain(subdomain) + if err != nil { + return nil, err + } + // get user ID + user, err := dbpool.FindUserForName(props.Username) + if err != nil { + return nil, err + } + + projectID := "" + postID := "" + if space == "pgs" { // figure out project ID + project, err := dbpool.FindProjectByName(user.ID, props.ProjectName) + if err != nil { + return nil, err + } + projectID = project.ID + } else if space == "prose" { // figure out post ID + if path == "" || path == "/" { + } else { + post, err := dbpool.FindPostWithSlug(path, user.ID, space) + if err != nil { + return nil, err + } + postID = post.ID + } + } + + return &db.AnalyticsVisits{ + UserID: user.ID, + ProjectID: projectID, + PostID: postID, + Namespace: space, + Host: host, + Path: path, + IpAddress: access.Request.ClientIP, + UserAgent: strings.Join(access.Request.Headers.UserAgent, " "), + Referer: strings.Join(access.Request.Headers.Referer, " "), + ContentType: strings.Join(access.RespHeaders.ContentType, " "), + Status: access.Status, + }, nil +} + +// this feels really stupid because i'm taking containter-drain, +// filtering it, and then sending it to metric-drain. The +// metricDrainSub function listens on the metric-drain and saves it. +// So why not just call the necessary functions to save the visit? +// We want to be able to use pipe as a debugging tool which means we +// can manually sub to `metric-drain` and have a nice clean output to view. +func containerDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger) { + info := shared.NewPicoPipeClient() + drain := pipe.NewReconnectReadWriteCloser( + ctx, + logger, + info, + "container drain", + "sub container-drain -k", + 100, + -1, + ) + + send := pipe.NewReconnectReadWriteCloser( + ctx, + logger, + info, + "from container drain to metric drain", + "pub metric-drain -b=false", + 100, + -1, + ) + + for { + scanner := bufio.NewScanner(drain) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "http.log.access") { + clean := strings.TrimSpace(line) + visit, err := accessLogToVisit(dbpool, clean) + if err != nil { + logger.Debug("could not convert access log to a visit", "err", err) + continue + } + jso, err := json.Marshal(visit) + if err != nil { + logger.Error("could not marshal json of a visit", "err", err) + continue + } + _, _ = send.Write(jso) + } + } + } +} + +func accessLogToVisit(dbpool db.DB, line string) (*db.AnalyticsVisits, error) { + accessLog := CaddyAccessLog{} + err := json.Unmarshal([]byte(line), &accessLog) + if err != nil { + return nil, err + } + + return deserializeCaddyAccessLog(dbpool, &accessLog) +} + func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) { drain := metrics.ReconnectReadMetrics( ctx, visit := db.AnalyticsVisits{} err := json.Unmarshal([]byte(line), &visit) if err != nil { - logger.Error("json unmarshal", "err", err) + logger.Info("could not unmarshal json", "err", err, "line", line) continue } - - user := slog.Any("userId", visit.UserID) - err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret) if err != nil { if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Info("could not record analytics visit", "reason", err, "visit", visit, user) - continue + logger.Info("could not record analytics visit", "reason", err) } } - logger.Info("inserting visit", "visit", visit, user) + if visit.ContentType != "" && !strings.HasPrefix(visit.ContentType, "text/html") { + continue + } + + logger.Info("inserting visit", "visit", visit) err = dbpool.InsertVisit(&visit) if err != nil { - logger.Error("could not insert visit record", "err", err, "visit", visit, user) + logger.Error("could not insert visit record", "err", err) } } - - if scanner.Err() != nil { - logger.Error("scanner error", "err", scanner.Err()) - } } } // gather metrics in the auth service go metricDrainSub(ctx, db, logger, cfg.Secret) + // convert container logs to access logs + go containerDrainSub(ctx, db, logger) + defer ctx.Done() apiConfig := &shared.ApiConfig{ @@ db/db.go } type AnalyticsVisits struct { - ID string `json:"id"` - UserID string `json:"user_id"` - ProjectID string `json:"project_id"` - PostID string `json:"post_id"` - Namespace string `json:"namespace"` - Host string `json:"host"` - Path string `json:"path"` - IpAddress string `json:"ip_address"` - UserAgent string `json:"user_agent"` - Referer string `json:"referer"` - Status int `json:"status"` + ID string `json:"id"` + UserID string `json:"user_id"` + ProjectID string `json:"project_id"` + PostID string `json:"post_id"` + Namespace string `json:"namespace"` + Host string `json:"host"` + Path string `json:"path"` + IpAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Referer string `json:"referer"` + Status int `json:"status"` + ContentType string `json:"content_type"` } type VisitInterval struct { @@ db/postgres/storage.go func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error { _, err := me.Db.Exec( - `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);`, + `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status, content_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);`, visit.UserID, newNullString(visit.ProjectID), newNullString(visit.PostID), visit.UserAgent, visit.Referer, visit.Status, + visit.ContentType, ) return err } @@ imgs/api.go dbpool := shared.GetDB(r) logger := shared.GetLogger(r) username := shared.GetUsernameFromRequest(r) - analytics := shared.GetAnalyticsQueue(r) user, err := dbpool.FindUserForName(username) if err != nil { logger, dbpool, st, - analytics, ) router.ServeAsset(fname, opts, true, anyPerm, w, r) } @@ pastes/api.go Unlisted bool } -type TransparencyPageData struct { - Site shared.SitePageData - Analytics *db.Analytics -} - type Link struct { URL string Text string @@ pgs/ssh.go "github.com/charmbracelet/promwish" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" - "github.com/picosh/pico/db" "github.com/picosh/pico/db/postgres" "github.com/picosh/pico/shared" "github.com/picosh/pico/shared/storage" st, ) - ch := make(chan *db.AnalyticsVisits, 100) - go shared.AnalyticsCollect(ch, dbpool, logger) apiConfig := &shared.ApiConfig{ - Cfg: cfg, - Dbpool: dbpool, - Storage: st, - AnalyticsQueue: ch, + Cfg: cfg, + Dbpool: dbpool, + Storage: st, } webTunnel := &tunkit.WebTunnelHandler{ @@ pgs/tunnel.go "pubkey", pubkeyStr, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { log.Error(err.Error()) return http.HandlerFunc(shared.UnauthorizedHandler) logger, apiConfig.Dbpool, apiConfig.Storage, - apiConfig.AnalyticsQueue, ) tunnelRouter := TunnelWebRouter{routes} router := http.NewServeMux() @@ pgs/web.go return } - ch := make(chan *db.AnalyticsVisits, 100) - go shared.AnalyticsCollect(ch, dbpool, logger) - - routes := NewWebRouter(cfg, logger, dbpool, st, ch) + routes := NewWebRouter(cfg, logger, dbpool, st) portStr := fmt.Sprintf(":%s", cfg.Port) logger.Info( type HasPerm = func(proj *db.Project) bool type WebRouter struct { - Cfg *shared.ConfigSite - Logger *slog.Logger - Dbpool db.DB - Storage storage.StorageServe - AnalyticsQueue chan *db.AnalyticsVisits - RootRouter *http.ServeMux - UserRouter *http.ServeMux + Cfg *shared.ConfigSite + Logger *slog.Logger + Dbpool db.DB + Storage storage.StorageServe + RootRouter *http.ServeMux + UserRouter *http.ServeMux } -func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe, analytics chan *db.AnalyticsVisits) *WebRouter { +func NewWebRouter(cfg *shared.ConfigSite, logger *slog.Logger, dbpool db.DB, st storage.StorageServe) *WebRouter { router := &WebRouter{ - Cfg: cfg, - Logger: logger, - Dbpool: dbpool, - Storage: st, - AnalyticsQueue: analytics, + Cfg: cfg, + Logger: logger, + Dbpool: dbpool, + Storage: st, } router.initRouters() return router if !strings.Contains(hostDomain, appDomain) { subdomain := shared.GetCustomDomain(hostDomain, cfg.Space) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Error( "could not get project from subdomain", "host", r.Host, ) - props, err := getProjectFromSubdomain(subdomain) + props, err := shared.GetProjectFromSubdomain(subdomain) if err != nil { logger.Info( "could not determine project from subdomain", ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain) router.ServeHTTP(w, r.WithContext(ctx)) } - -type SubdomainProps struct { - ProjectName string - Username string -} - -func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { - props := &SubdomainProps{} - strs := strings.SplitN(subdomain, "-", 2) - props.Username = strs[0] - if len(strs) == 2 { - props.ProjectName = strs[1] - } else { - props.ProjectName = props.Username - } - return props, nil -} @@ pgs/web_asset_handler.go package pgs import ( - "errors" "fmt" "io" "log/slog" "net/http/httputil" _ "net/http/pprof" - "github.com/picosh/pico/shared" "github.com/picosh/pico/shared/storage" sst "github.com/picosh/pobj/storage" ) "routes", strings.Join(attempts, ", "), "status", http.StatusNotFound, ) - // track 404s - ch := h.AnalyticsQueue - view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID) - if err == nil { - view.ProjectID = h.ProjectID - view.Status = http.StatusNotFound - select { - case ch <- view: - default: - logger.Error("could not send analytics view to channel", "view", view) - } - } else { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Error("could not record analytics view", "err", err, "view", view) - } - } http.Error(w, "404 not found", http.StatusNotFound) return } finContentType := w.Header().Get("content-type") - // only track pages, not individual assets - if finContentType == "text/html" { - // track visit - ch := h.AnalyticsQueue - view, err := shared.AnalyticsVisitFromRequest(r, h.Dbpool, h.UserID) - if err == nil { - view.ProjectID = h.ProjectID - select { - case ch <- view: - default: - logger.Error("could not send analytics view to channel", "view", view) - } - } else { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Error("could not record analytics view", "err", err, "view", view) - } - } - } - logger.Info( "serving asset", "asset", assetFilepath, @@ pgs/web_test.go "net/http/httptest" "strings" "testing" - "time" "github.com/picosh/pico/db" "github.com/picosh/pico/db/stub" responseRecorder := httptest.NewRecorder() st, _ := storage.NewStorageMemory(tc.storage) - ch := make(chan *db.AnalyticsVisits, 100) - router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch) + router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != tc.status { } } -func TestAnalytics(t *testing.T) { - bucketName := shared.GetAssetBucketName(testUserID) - cfg := NewConfigSite() - cfg.Domain = "pgs.test" - expectedPath := "/app" - request := httptest.NewRequest("GET", mkpath(expectedPath), strings.NewReader("")) - responseRecorder := httptest.NewRecorder() - - sto := map[string]map[string]string{ - bucketName: { - "test/app.html": "hello world!", - }, - } - st, _ := storage.NewStorageMemory(sto) - ch := make(chan *db.AnalyticsVisits, 100) - dbpool := NewPgsAnalticsDb(cfg.Logger) - router := NewWebRouter(cfg, cfg.Logger, dbpool, st, ch) - - go func() { - for analytics := range ch { - if analytics.Path != expectedPath { - t.Errorf("Want path '%s', got '%s'", expectedPath, analytics.Path) - } - close(ch) - } - }() - - router.ServeHTTP(responseRecorder, request) - - select { - case <-ch: - return - case <-time.After(time.Second * 1): - t.Error("didnt receive analytics event within time limit") - } -} - type ImageStorageMemory struct { *storage.StorageMemory Opts *storage.ImgProcessOpts Ratio: &storage.Ratio{}, }, } - ch := make(chan *db.AnalyticsVisits, 100) - router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st, ch) + router := NewWebRouter(cfg, cfg.Logger, tc.dbpool, st) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != tc.status { @@ prose/api.go import ( "bytes" - "errors" "fmt" "html/template" "net/http" Diff template.HTML } -type TransparencyPageData struct { - Site shared.SitePageData - Analytics *db.Analytics -} - type HeaderTxt struct { Title string Bio string postCollection = append(postCollection, p) } - // track visit - ch := shared.GetAnalyticsQueue(r) - view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID) - if err == nil { - select { - case ch <- view: - default: - logger.Error("could not send analytics view to channel", "view", view) - } - } else { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Error("could not record analytics view", "err", err, "view", view) - } - } - data := BlogPageData{ Site: *cfg.GetSiteData(), PageTitle: headerTxt.Title, username := shared.GetUsernameFromRequest(r) subdomain := shared.GetSubdomain(r) cfg := shared.GetCfg(r) - ch := shared.GetAnalyticsQueue(r) var slug string if !cfg.IsSubdomains() || subdomain == "" { ogImageCard = parsedText.ImageCard } - // track visit - view, err := shared.AnalyticsVisitFromRequest(r, dbpool, user.ID) - if err == nil { - view.PostID = post.ID - select { - case ch <- view: - default: - logger.Error("could not send analytics view to channel", "view", view) - } - } else { - if !errors.Is(err, shared.ErrAnalyticsDisabled) { - logger.Error("could not record analytics view", "err", err, "view", view) - } - } - unlisted := false if post.Hidden || post.PublishAt.After(time.Now()) { unlisted = true mainRoutes := createMainRoutes(staticRoutes) subdomainRoutes := createSubdomainRoutes(staticRoutes) - ch := make(chan *db.AnalyticsVisits, 100) - go shared.AnalyticsCollect(ch, dbpool, logger) apiConfig := &shared.ApiConfig{ - Cfg: cfg, - Dbpool: dbpool, - Storage: st, - AnalyticsQueue: ch, + Cfg: cfg, + Dbpool: dbpool, + Storage: st, } handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig) router := http.HandlerFunc(handler) @@ shared/api.go "github.com/picosh/utils" ) +type SubdomainProps struct { + ProjectName string + Username string +} + +func GetProjectFromSubdomain(subdomain string) (*SubdomainProps, error) { + props := &SubdomainProps{} + strs := strings.SplitN(subdomain, "-", 2) + props.Username = strs[0] + if len(strs) == 2 { + props.ProjectName = strs[1] + } else { + props.ProjectName = props.Username + } + return props, nil +} + func CorsHeaders(headers http.Header) { headers.Add("Access-Control-Allow-Origin", "*") headers.Add("Vary", "Origin") @@ shared/router.go } type ApiConfig struct { - Cfg *ConfigSite - Dbpool db.DB - Storage storage.StorageServe - AnalyticsQueue chan *db.AnalyticsVisits + Cfg *ConfigSite + Dbpool db.DB + Storage storage.StorageServe } func (hc *ApiConfig) HasPrivilegedAccess(apiToken string) bool { ctx = context.WithValue(ctx, ctxDBKey{}, hc.Dbpool) ctx = context.WithValue(ctx, ctxStorageKey{}, hc.Storage) ctx = context.WithValue(ctx, ctxCfg{}, hc.Cfg) - ctx = context.WithValue(ctx, ctxAnalyticsQueue{}, hc.AnalyticsQueue) return ctx } type ctxStorageKey struct{} type ctxLoggerKey struct{} type ctxCfg struct{} -type ctxAnalyticsQueue struct{} type CtxSubdomainKey struct{} type ctxKey struct{} return "" } -func GetAnalyticsQueue(r *http.Request) chan *db.AnalyticsVisits { - return r.Context().Value(ctxAnalyticsQueue{}).(chan *db.AnalyticsVisits) -} - func GetApiToken(r *http.Request) string { authHeader := r.Header.Get("authorization") if authHeader == "" { @@ sql/migrations/20241125_add_content_type_to_analytics.sql +ALTER TABLE analytics_visits ADD COLUMN content_type varchar(256);
Patchset ps-74
reactor(metric-drain): use caddy json format
Eric Bower
auth/api.go
+92
-4
caddy.json
+40
-0
pgs/tunnel.go
+1
-1
pgs/web.go
+2
-19
shared/api.go
+17
-0
reactor(metric-drain): use caddy json format
auth/api.go
link
+92
-4
+92
-4
1diff --git a/auth/api.go b/auth/api.go
2index aad4962..c6fef59 100644
3--- a/auth/api.go
4+++ b/auth/api.go
5@@ -642,6 +642,89 @@ func handler(routes []shared.Route, client *Client) http.HandlerFunc {
6 }
7 }
8
9+type AccessLogReq struct {
10+ RemoteIP string `json:"remote_ip"`
11+ RemotePort string `json:"remote_port"`
12+ ClientIP string `json:"client_ip"`
13+ Method string `json:"method"`
14+ Host string `json:"host"`
15+ Uri string `json:"uri"`
16+ Headers struct {
17+ UserAgent string `json:"User-Agent"`
18+ Referer string `json:"Referer"`
19+ } `json:"headers"`
20+ Tls struct {
21+ ServerName string `json:"server_name"`
22+ } `json:"tls"`
23+}
24+
25+type CaddyAccessLog struct {
26+ Request AccessLogReq `json:"request"`
27+ Status int `json:"status"`
28+}
29+
30+func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) {
31+ spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2)
32+ space := spaceRaw[0]
33+ host := access.Request.Host
34+ path := access.Request.Uri
35+ subdomain := ""
36+
37+ // grab subdomain based on host
38+ if strings.HasSuffix(host, "tuns.sh") {
39+ subdomain = strings.TrimSuffix(host, ".tuns.sh")
40+ } else if strings.HasSuffix(host, "pgs.sh") {
41+ subdomain = strings.TrimSuffix(host, ".pgs.sh")
42+ } else if strings.HasSuffix(host, "prose.sh") {
43+ subdomain = strings.TrimSuffix(host, ".prose.sh")
44+ } else {
45+ subdomain = shared.GetCustomDomain(host, space)
46+ }
47+
48+ // get user and namespace details from subdomain
49+ props, err := shared.GetProjectFromSubdomain(subdomain)
50+ if err != nil {
51+ return nil, err
52+ }
53+ // get user ID
54+ user, err := dbpool.FindUserForName(props.Username)
55+ if err != nil {
56+ return nil, err
57+ }
58+
59+ projectID := ""
60+ postID := ""
61+ if space == "pgs" { // figure out project ID
62+ project, err := dbpool.FindProjectByName(user.ID, props.ProjectName)
63+ if err != nil {
64+ return nil, err
65+ }
66+ projectID = project.ID
67+ } else if space == "prose" { // figure out post ID
68+ if path == "" || path == "/" {
69+ } else {
70+ post, err := dbpool.FindPostWithSlug(path, user.ID, space)
71+ if err != nil {
72+ return nil, err
73+ }
74+ postID = post.ID
75+ }
76+ }
77+
78+ return &db.AnalyticsVisits{
79+ UserID: user.ID,
80+ ProjectID: projectID,
81+ PostID: postID,
82+ Namespace: space,
83+ Host: host,
84+ Path: path,
85+ IpAddress: access.Request.ClientIP,
86+ UserAgent: access.Request.Headers.UserAgent,
87+ Referer: access.Request.Headers.Referer, // TODO: I don't see referer in the access log
88+ Status: access.Status,
89+ }, nil
90+}
91+
92 func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) {
93 conn := shared.NewPicoPipeClient()
94 stdoutPipe, err := pubsub.RemoteSub("sub metric-drain -k", ctx, conn)
95@@ -654,14 +737,19 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr
96 scanner := bufio.NewScanner(stdoutPipe)
97 for scanner.Scan() {
98 line := scanner.Text()
99- visit := db.AnalyticsVisits{}
100- err := json.Unmarshal([]byte(line), &visit)
101+ accessLog := CaddyAccessLog{}
102+ err := json.Unmarshal([]byte(line), &accessLog)
103 if err != nil {
104 logger.Error("json unmarshal", "err", err)
105 continue
106 }
107
108- err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret)
109+ visit, err := deserializeCaddyAccessLog(dbpool, &accessLog)
110+ if err != nil {
111+ logger.Error("cannot deserialize access log", "err", err)
112+ continue
113+ }
114+ err = shared.AnalyticsVisitFromVisit(visit, dbpool, secret)
115 if err != nil {
116 if !errors.Is(err, shared.ErrAnalyticsDisabled) {
117 logger.Info("could not record analytics visit", "reason", err)
118@@ -669,7 +757,7 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr
119 }
120
121 logger.Info("inserting visit", "visit", visit)
122- err = dbpool.InsertVisit(&visit)
123+ err = dbpool.InsertVisit(visit)
124 if err != nil {
125 logger.Error("could not insert visit record", "err", err)
126 }
caddy.json
link
+40
-0
+40
-0
1diff --git a/caddy.json b/caddy.json
2new file mode 100644
3index 0000000..49e80ec
4--- /dev/null
5+++ b/caddy.json
6@@ -0,0 +1,40 @@
7+{
8+ "level": "info",
9+ "ts": 1731644477.313701,
10+ "logger": "http.log.access",
11+ "msg": "handled request",
12+ "request": {
13+ "remote_ip": "127.0.0.1",
14+ "remote_port": "40400",
15+ "client_ip": "127.0.0.1",
16+ "proto": "HTTP/2.0",
17+ "method": "GET",
18+ "host": "pgs.sh",
19+ "uri": "/",
20+ "headers": { "User-Agent": ["Blackbox Exporter/0.24.0"] },
21+ "tls": {
22+ "resumed": false,
23+ "version": 772,
24+ "cipher_suite": 4865,
25+ "proto": "h2",
26+ "server_name": "pgs.sh"
27+ }
28+ },
29+ "bytes_read": 0,
30+ "user_id": "",
31+ "duration": 0.001207084,
32+ "size": 3718,
33+ "status": 200,
34+ "resp_headers": {
35+ "Referrer-Policy": ["no-referrer-when-downgrade"],
36+ "Strict-Transport-Security": ["max-age=31536000;"],
37+ "X-Content-Type-Options": ["nosniff"],
38+ "X-Frame-Options": ["DENY"],
39+ "Server": ["Caddy"],
40+ "Alt-Svc": ["h3=\":443\"; ma=2592000"],
41+ "Date": ["Fri, 15 Nov 2024 04:21:17 GMT"],
42+ "Content-Type": ["text/html; charset=utf-8"],
43+ "X-Xss-Protection": ["1; mode=block"],
44+ "Permissions-Policy": ["interest-cohort=()"]
45+ }
46+}
pgs/tunnel.go
link
+1
-1
+1
-1
1diff --git a/pgs/tunnel.go b/pgs/tunnel.go
2index b635c8e..accacc5 100644
3--- a/pgs/tunnel.go
4+++ b/pgs/tunnel.go
5@@ -51,7 +51,7 @@ func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
6 "pubkey", pubkeyStr,
7 )
8
9- props, err := getProjectFromSubdomain(subdomain)
10+ props, err := shared.GetProjectFromSubdomain(subdomain)
11 if err != nil {
12 log.Error(err.Error())
13 return http.HandlerFunc(shared.UnauthorizedHandler)
pgs/web.go
link
+2
-19
+2
-19
1diff --git a/pgs/web.go b/pgs/web.go
2index cd251f0..6f3aceb 100644
3--- a/pgs/web.go
4+++ b/pgs/web.go
5@@ -174,7 +174,7 @@ func (web *WebRouter) checkHandler(w http.ResponseWriter, r *http.Request) {
6
7 if !strings.Contains(hostDomain, appDomain) {
8 subdomain := shared.GetCustomDomain(hostDomain, cfg.Space)
9- props, err := getProjectFromSubdomain(subdomain)
10+ props, err := shared.GetProjectFromSubdomain(subdomain)
11 if err != nil {
12 logger.Error(
13 "could not get project from subdomain",
14@@ -330,7 +330,7 @@ func (web *WebRouter) ServeAsset(fname string, opts *storage.ImgProcessOpts, fro
15 "host", r.Host,
16 )
17
18- props, err := getProjectFromSubdomain(subdomain)
19+ props, err := shared.GetProjectFromSubdomain(subdomain)
20 if err != nil {
21 logger.Info(
22 "could not determine project from subdomain",
23@@ -447,20 +447,3 @@ func (web *WebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
24 ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain)
25 router.ServeHTTP(w, r.WithContext(ctx))
26 }
27-
28-type SubdomainProps struct {
29- ProjectName string
30- Username string
31-}
32-
33-func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) {
34- props := &SubdomainProps{}
35- strs := strings.SplitN(subdomain, "-", 2)
36- props.Username = strs[0]
37- if len(strs) == 2 {
38- props.ProjectName = strs[1]
39- } else {
40- props.ProjectName = props.Username
41- }
42- return props, nil
43-}