diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 719de55..8a30288 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -33,6 +33,8 @@ services: - DATA_MANAGER_URL=htwkalender-data-manager networks: - "net" + depends_on: + - htwkalender-data-manager htwkalender-frontend: image: DOCKER_REGISTRY_REPO-frontend # DOCKER_REGISTRY_REPO will be replaced by CI diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f52e1aa..061692b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,6 +33,8 @@ services: - DATA_MANAGER_URL=htwkalender-data-manager networks: - "net" + depends_on: + - htwkalender-data-manager htwkalender-frontend: image: DOCKER_REGISTRY_REPO-frontend # DOCKER_REGISTRY_REPO will be replaced by CI diff --git a/docker-compose.yml b/docker-compose.yml index ddb62a0..01f70bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,8 @@ services: target: dev # prod environment: - DATA_MANAGER_URL=htwkalender-data-manager + depends_on: + - htwkalender-data-manager htwkalender-frontend: build: diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 334df1a..dc99832 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -27,11 +27,10 @@ http { include mime.types; default_type application/octet-stream; - # Define a custom log format for anonymizing logs log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"'; access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized; - error_log /opt/bitnami/nginx/logs/proxy_error.log; + error_log /opt/bitnami/nginx/logs/proxy_error.log error; sendfile on; keepalive_timeout 180s; diff --git a/reverseproxy.conf b/reverseproxy.conf index 386ab70..71332dc 100644 --- a/reverseproxy.conf +++ b/reverseproxy.conf @@ -59,11 +59,10 @@ http { real_ip_header CF-Connecting-IP; - # Define a custom log format for anonymizing logs log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"'; access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized; - error_log /opt/bitnami/nginx/logs/proxy_error.log; + error_log /opt/bitnami/nginx/logs/proxy_error.log error; sendfile on; keepalive_timeout 180s; diff --git a/reverseproxy.dev.conf b/reverseproxy.dev.conf index 793ce4e..d735cad 100644 --- a/reverseproxy.dev.conf +++ b/reverseproxy.dev.conf @@ -60,8 +60,10 @@ http { real_ip_header CF-Connecting-IP; - access_log /opt/bitnami/nginx/logs/proxy_access.log; - error_log /opt/bitnami/nginx/logs/proxy_error.log; + log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"'; + + access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized; + error_log /opt/bitnami/nginx/logs/proxy_error.log error; sendfile on; keepalive_timeout 180s; diff --git a/reverseproxy.local.conf b/reverseproxy.local.conf index 60b3822..42a12cf 100644 --- a/reverseproxy.local.conf +++ b/reverseproxy.local.conf @@ -10,6 +10,12 @@ events { http { include mime.types; default_type application/octet-stream; + gzip on; + + log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"'; + + access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized; + error_log /opt/bitnami/nginx/logs/proxy_error.log error; map $request_method $ratelimit_key { POST $binary_remote_addr; diff --git a/services/data-manager/service/grpc/server.go b/services/data-manager/service/grpc/server.go index f5b6ad3..9992ec0 100644 --- a/services/data-manager/service/grpc/server.go +++ b/services/data-manager/service/grpc/server.go @@ -2,8 +2,10 @@ package grpc import ( "github.com/pocketbase/pocketbase" + "google.golang.org/grpc/keepalive" "log" "net" + "time" "google.golang.org/grpc" pb "htwkalender/common/genproto/modules" @@ -14,7 +16,20 @@ func StartGRPCServer(app *pocketbase.PocketBase) { if err != nil { log.Fatalf("failed to listen: %v", err) } - s := grpc.NewServer() + s := grpc.NewServer( + grpc.KeepaliveParams(keepalive.ServerParameters{ + MaxConnectionIdle: 5 * time.Minute, // Idle timeout before closing connection + MaxConnectionAge: 30 * time.Minute, // Max time before connection is closed + MaxConnectionAgeGrace: 5 * time.Minute, // Allow grace period before closing + Time: 2 * time.Minute, // Ping the client every 2 minutes + Timeout: 20 * time.Second, // Wait 20 seconds for ping ack + }), + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: 1 * time.Minute, // Minimum time between pings from clients + PermitWithoutStream: true, // Don't allow pings when there are no active RPCs + }), + grpc.MaxConcurrentStreams(0), + ) pb.RegisterModuleServiceServer(s, &ModuleServiceHandler{ app: app, diff --git a/services/ical/main.go b/services/ical/main.go index 1769786..e0a4d10 100644 --- a/services/ical/main.go +++ b/services/ical/main.go @@ -39,8 +39,15 @@ func main() { } grpcClient := grpc.ConnectGRPCServer(host) + + // Close the grpc connection when the main function ends defer grpc.CloseGRPCServer(grpcClient) + // Log the grpc connection + // Test the connection to the grpc server + grpcClient.Connect() + slog.Info("GRPC connection state", "state", grpcClient.GetState()) + // Initialize a new Fiber app webdavRequestMethods := []string{"PROPFIND", "MKCOL", "COPY", "MOVE"} diff --git a/services/ical/model/eventModel.go b/services/ical/model/eventModel.go index 2d92a0d..511ad22 100644 --- a/services/ical/model/eventModel.go +++ b/services/ical/model/eventModel.go @@ -37,6 +37,14 @@ func (events Events) Sort() { }) } +func (events Events) String() string { + var str strings.Builder + for _, event := range events { + str.WriteString(event.String()) + } + return str.String() +} + type AnonymizedEventDTO struct { Day string `db:"Day" json:"day"` Week string `db:"Week" json:"week"` @@ -109,3 +117,7 @@ func (e *Event) GetName() string { func (e *Event) SetName(name string) { e.Name = name } + +func (e *Event) String() string { + return e.UUID + e.Day + e.Week + e.Start.String() + e.End.String() + e.Name + e.EventType + e.Compulsory + e.Prof + e.Rooms + e.Notes + e.BookedAt + e.Course + e.Semester +} diff --git a/services/ical/model/icalModel.go b/services/ical/model/icalModel.go index 14121fd..e5cab49 100644 --- a/services/ical/model/icalModel.go +++ b/services/ical/model/icalModel.go @@ -92,3 +92,7 @@ func ToJSONTime(timeString string) JSONTime { } return JSONTime(t) } + +func (j JSONTime) String() string { + return time.Time(j).Format(DefaultDateLayout) +} diff --git a/services/ical/service/connector/grpc/client.go b/services/ical/service/connector/grpc/client.go index 7b3b261..1954523 100644 --- a/services/ical/service/connector/grpc/client.go +++ b/services/ical/service/connector/grpc/client.go @@ -19,14 +19,31 @@ package grpc import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" "log/slog" + "sync" + "time" ) +var conn *grpc.ClientConn +var once sync.Once + func ConnectGRPCServer(host string) *grpc.ClientConn { - conn, err := grpc.NewClient(host+":50051", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - slog.Error("could not connect to grpc server", "error", err) - } + once.Do(func() { + var err error + conn, err = grpc.NewClient( + host+":50051", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 2 * time.Minute, + Timeout: 20 * time.Second, + PermitWithoutStream: true, + }), + ) + if err != nil { + slog.Error("could not connect to grpc server", "error", err) + } + }) return conn } diff --git a/services/ical/service/ical/ical.go b/services/ical/service/ical/ical.go index ba8e7f2..11759e0 100644 --- a/services/ical/service/ical/ical.go +++ b/services/ical/service/ical/ical.go @@ -23,6 +23,7 @@ import ( "htwkalender/ical/model" "htwkalender/ical/service/connector" htwkalenderGrpc "htwkalender/ical/service/connector/grpc" + "htwkalender/ical/service/functions" "log/slog" "time" ) @@ -31,7 +32,7 @@ const expirationTime = 5 * time.Minute var FeedDeletedError = fmt.Errorf("feed deleted") -func Feed(app model.AppType, token string) (string, error) { +func Feed(app model.AppType, token string) (string, string, error) { var events model.Events modules := map[string]model.FeedCollection{} @@ -47,17 +48,17 @@ func Feed(app model.AppType, token string) (string, error) { // get feed by token feed, err := htwkalenderGrpc.GetFeed(token, app.GrpcClient) if err != nil { - return "", err + return "", "", err } if feed.Deleted { - return "", FeedDeletedError + return "", "", FeedDeletedError } // Get all events for modules events, err = htwkalenderGrpc.GetEvents(feed.Modules, app.GrpcClient) if err != nil { - return "", err + return "", "", err } // Sort events by start date @@ -68,11 +69,14 @@ func Feed(app model.AppType, token string) (string, error) { } } + // Generate one Hash for E-TAG from all events and modules + etag := functions.HashString(events.String() + fmt.Sprint(modules)) + b := bytes.Buffer{} goics.NewICalEncode(&b).Encode(IcalModel{Events: events, Mapping: modules}) icalFeed := &model.FeedModel{Content: b.String(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))} - return icalFeed.Content, nil + return icalFeed.Content, etag, nil } func FeedRecord(app model.AppType, token string) (model.FeedRecord, error) { diff --git a/services/ical/service/routes.go b/services/ical/service/routes.go index c92a0f7..f7da962 100644 --- a/services/ical/service/routes.go +++ b/services/ical/service/routes.go @@ -34,7 +34,8 @@ func AddFeedRoutes(app model.AppType) { app.Fiber.Get("/api/feed", func(c fiber.Ctx) error { token := c.Query("token") - results, err := ical.Feed(app, token) + ifNoneMatch := c.Get("If-None-Match") + results, eTag, err := ical.Feed(app, token) if errors.Is(err, ical.FeedDeletedError) { return c.SendStatus(fiber.StatusGone) @@ -44,10 +45,16 @@ func AddFeedRoutes(app model.AppType) { slog.Error("Failed to get feed", "error", err, "token", token) return c.SendStatus(fiber.StatusNotFound) } + + if ifNoneMatch == eTag && ifNoneMatch != "" { + return c.SendStatus(fiber.StatusNotModified) + } + c.Response().Header.Set("Content-type", "text/calendar") c.Response().Header.Set("charset", "utf-8") c.Response().Header.Set("Content-Disposition", "inline") c.Response().Header.Set("filename", "calendar.ics") + c.Response().Header.Set("ETag", eTag) return c.SendString(results) })