Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore:less boilerplate heavy ios notifications #272

Merged
merged 8 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,290 changes: 644 additions & 646 deletions server/api/tumdev/campus_backend.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/api/tumdev/campus_backend.proto
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ enum DeviceType {

message CreateDeviceRequest {
string device_id = 1;
optional string public_key = 2;
string public_key = 2;
DeviceType device_type = 3;
}

Expand Down
14 changes: 7 additions & 7 deletions server/backend/cafeteria.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func queryTags(cafeteriaID int32, dishID int32, ratingType ModelType, tx *gorm.D
}

if err != nil {
log.WithError(err).Error("while querying the tags for the request.")
log.WithError(err).Error("while querying the tags for the request")
}

//needed since the gRPC element does not specify column names - cannot be directly queried into the grpc message object.
Expand Down Expand Up @@ -672,25 +672,25 @@ func (s *CampusServer) GetCafeterias(ctx context.Context, _ *pb.ListCanteensRequ
}, requestStatus
}

func (s *CampusServer) ListDishes(ctx context.Context, request *pb.ListDishesRequest) (*pb.ListDishesReply, error) {
if request.Year < 2022 {
func (s *CampusServer) ListDishes(ctx context.Context, req *pb.ListDishesRequest) (*pb.ListDishesReply, error) {
if req.Year < 2022 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Years must be larger or equal to 2022 ") // currently, no previous values have been added
}
if request.Week < 1 || request.Week > 53 {
if req.Week < 1 || req.Week > 53 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Weeks must be in the range 1 - 53")
}
if request.Day < 0 || request.Day > 4 {
if req.Day < 0 || req.Day > 4 {
return &pb.ListDishesReply{}, status.Error(codes.Internal, "Days must be in the range 1 (Monday) - 4 (Friday)")
}

var requestStatus error = nil
var results []string
err := s.db.WithContext(ctx).Table("dishes_of_the_week weekly").
Where("weekly.day = ? AND weekly.week = ? and weekly.year = ?", request.Day, request.Week, request.Year).
Where("weekly.day = ? AND weekly.week = ? and weekly.year = ?", req.Day, req.Week, req.Year).
Select("weekly.dishID").
Joins("JOIN dish d ON d.dish = weekly.dishID").
Joins("JOIN cafeteria c ON c.cafeteria = d.cafeteriaID").
Where("c.name LIKE ?", request.CanteenId).
Where("c.name LIKE ?", req.CanteenId).
Select("d.name").
Find(&results).Error

Expand Down
5 changes: 4 additions & 1 deletion server/backend/campus_api/campus_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"fmt"
"io"
"net/http"
"os"

"github.com/TUM-Dev/Campus-Backend/server/model"
log "github.com/sirupsen/logrus"
)

func FetchExamResultsPublished(token string) (*model.TUMAPIPublishedExamResults, error) {
// FetchExamResultsPublished fetches all published exam results from the TUM Campus API using CAMPUS_API_TOKEN.
func FetchExamResultsPublished() (*model.TUMAPIPublishedExamResults, error) {
var examResultsPublished model.TUMAPIPublishedExamResults
token := os.Getenv("CAMPUS_API_TOKEN")
err := RequestCampusApi("/wbservicesbasic.pruefungenErgebnisse", token, &examResultsPublished)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions server/backend/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (s *CampusServer) CreateDevice(_ context.Context, req *pb.CreateDeviceReque
return nil, status.Error(codes.InvalidArgument, err.Error())
}

switch req.GetDeviceType() {
switch req.DeviceType {
case pb.DeviceType_ANDROID:
return nil, status.Error(codes.Unimplemented, "android device creation not implemented")
case pb.DeviceType_IOS:
Expand All @@ -146,7 +146,7 @@ func (s *CampusServer) DeleteDevice(_ context.Context, req *pb.DeleteDeviceReque
return nil, status.Error(codes.InvalidArgument, err.Error())
}

switch req.GetDeviceType() {
switch req.DeviceType {
case pb.DeviceType_ANDROID:
return nil, status.Error(codes.Unimplemented, "android device remove not implemented")
case pb.DeviceType_IOS:
Expand Down
34 changes: 10 additions & 24 deletions server/backend/ios_notifications/apns/jwt_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,6 @@ import (
log "github.com/sirupsen/logrus"
)

const (
// TokenTimeout for the token in seconds
TokenTimeout = 3000
)

var (
ErrorAuthKeyNotPem = errors.New("failed to parse token: AuthKey must be a valid .p8 PEM file")
ErrorAuthKeyNotEcdsa = errors.New("failed to parse token: AuthKey must be of type ecdsa.PrivateKey")
ErrorAuthKeyNil = errors.New("failed to parse token: AuthKey was nil")
ApnsKeyId = os.Getenv("APNS_KEY_ID")
ApnsTeamId = os.Getenv("APNS_TEAM_ID")
ApnsP8FilePath = os.Getenv("APNS_P8_FILE_PATH")
)

type JWTToken struct {
sync.Mutex
EncryptionKey *ecdsa.PrivateKey
Expand All @@ -39,15 +25,15 @@ type JWTToken struct {
}

func NewToken() (*JWTToken, error) {
encryptionKey, err := APNsEncryptionKeyFromFile()
encryptionKey, err := EncryptionKeyFromFile()
if err != nil {
return nil, err
}

token := JWTToken{
EncryptionKey: encryptionKey,
KeyId: ApnsKeyId,
TeamId: ApnsTeamId,
KeyId: os.Getenv("APNS_KEY_ID"),
TeamId: os.Getenv("APNS_TEAM_ID"),
}

if err = token.Generate(); err != nil {
Expand All @@ -57,11 +43,11 @@ func NewToken() (*JWTToken, error) {
return &token, nil
}

// APNsEncryptionKeyFromFile reads the APNs encryption key from the file system
// EncryptionKeyFromFile reads the APNs encryption key from the file system
// and returns it as an ecdsa.PrivateKey
// The file location is defined by the APNS_P8_FILE_PATH environment variable
func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
path, err := filepath.Abs(ApnsP8FilePath)
func EncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
path, err := filepath.Abs(os.Getenv("APNS_P8_FILE_PATH"))

if err != nil {
log.Error("No valid path to AuthKey")
Expand All @@ -81,7 +67,7 @@ func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
if block == nil {
log.Error("Could not decode APNs encryption key from file")

return nil, ErrorAuthKeyNotPem
return nil, errors.New("failed to parse token: AuthKey must be a valid .p8 PEM file")
}

key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
Expand All @@ -96,7 +82,7 @@ func APNsEncryptionKeyFromFile() (*ecdsa.PrivateKey, error) {
return ecdsaKey, nil
}

return nil, ErrorAuthKeyNotEcdsa
return nil, errors.New("failed to parse token: AuthKey must be of type ecdsa.PrivateKey")
}

func (t *JWTToken) GenerateNewTokenIfExpired() (bearer string) {
Expand All @@ -114,12 +100,12 @@ func (t *JWTToken) GenerateNewTokenIfExpired() (bearer string) {
}

func (t *JWTToken) IsExpired() bool {
return currentTimestamp() >= (t.IssuedAt + TokenTimeout)
return currentTimestamp() >= (t.IssuedAt + 3000)
}

func (t *JWTToken) Generate() error {
if t.EncryptionKey == nil {
return ErrorAuthKeyNil
return errors.New("failed to parse token: AuthKey was nil")
}

issuedAt := currentTimestamp()
Expand Down
53 changes: 12 additions & 41 deletions server/backend/ios_notifications/apns/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,6 @@ import (
"gorm.io/gorm"
)

const (
// BundleId from the Apple Developer Portal
BundleId = "de.tum.tca"
// ReadIdleTimeout is the idle time after which the http2 transport will do a health check
ReadIdleTimeout = 15 * time.Second
// HTTPClientTimeout is the timeout for the http client used to send notifications
HTTPClientTimeout = 60 * time.Second
)

const (
ApnsDevelopmentURL = "https://api.sandbox.push.apple.com:443"
ApnsProductionURL = "https://api.push.apple.com:443"
)

var (
ErrCouldNotSendNotification = errors.New("could not send notification")
ErrCouldNotDecodeAPNsResponse = errors.New("could not decode apns response")
)

type Repository struct {
DB gorm.DB
Token *JWTToken
Expand All @@ -43,11 +24,11 @@ type Repository struct {

// ApnsUrl uses the environment variable ENVIRONMENT to determine whether
// to use the production or development APNs URL.
func (r *Repository) ApnsUrl() string {
func (r *Repository) ApnsUrl(DeviceId string) string {
if env.IsProd() {
return ApnsProductionURL
return "https://api.push.apple.com:443/3/device/" + DeviceId
}
return ApnsDevelopmentURL
return "https://api.sandbox.push.apple.com:443/3/device/" + DeviceId
}

// CreateCampusTokenRequest creates a request log in the database that can be referred to
Expand All @@ -72,34 +53,24 @@ func (r *Repository) CreateRequest(deviceId string, requestType model.IOSBackgro
return &request, nil
}

func (r *Repository) SendAlertNotification(payload *model.IOSNotificationPayload) (*model.IOSRemoteNotificationResponse, error) {
return r.SendNotification(payload, model.IOSAPNSPushTypeAlert, 10)
}

func (r *Repository) SendBackgroundNotification(payload *model.IOSNotificationPayload) (*model.IOSRemoteNotificationResponse, error) {
return r.SendNotification(payload, model.IOSAPNSPushTypeBackground, 10)
}

func (r *Repository) SendNotification(notification *model.IOSNotificationPayload, apnsPushType model.IOSAPNSPushType, priority int) (*model.IOSRemoteNotificationResponse, error) {

url := r.ApnsUrl() + "/3/device/" + notification.DeviceId
func (r *Repository) SendNotification(notification *model.IOSNotificationPayload, apnsPushType model.IOSAPNSPushType) (*model.IOSRemoteNotificationResponse, error) {
body, _ := notification.MarshalJSON()

req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
req, _ := http.NewRequest(http.MethodPost, r.ApnsUrl(notification.DeviceId), bytes.NewBuffer(body))

// can be e.g. alert or background
req.Header.Set("apns-push-type", apnsPushType.String())
req.Header.Set("apns-topic", BundleId)
req.Header.Set("apns-topic", "de.tum.tca")
// can be a value between 1 and 10
req.Header.Set("apns-priority", strconv.Itoa(priority))
req.Header.Set("apns-priority", strconv.Itoa(10))

bearer := r.Token.GenerateNewTokenIfExpired()
req.Header.Set("authorization", "bearer "+bearer)

resp, err := r.httpClient.Do(req)
if err != nil {
log.WithError(err).Error("Could not send notification")
return nil, ErrCouldNotSendNotification
return nil, errors.New("could not send notification")
}
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
Expand All @@ -108,25 +79,25 @@ func (r *Repository) SendNotification(notification *model.IOSNotificationPayload
}(resp.Body)

var response model.IOSRemoteNotificationResponse
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil && err != io.EOF {
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil && err != io.EOF {
log.WithError(err).Error("Could not decode APNs response")
return nil, ErrCouldNotDecodeAPNsResponse
return nil, errors.New("could not decode apns response")
}

return &response, nil
}

func NewRepository(db *gorm.DB, token *JWTToken) *Repository {
transport := &http2.Transport{
ReadIdleTimeout: ReadIdleTimeout,
ReadIdleTimeout: 15 * time.Second,
}

return &Repository{
DB: *db,
Token: token,
httpClient: &http.Client{
Transport: transport,
Timeout: HTTPClientTimeout,
Timeout: 60 * time.Second,
},
}
}
Expand Down
22 changes: 8 additions & 14 deletions server/backend/ios_notifications/apns/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package apns

import (
"errors"
"os"

"github.com/TUM-Dev/Campus-Backend/server/model"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -32,30 +33,23 @@ func (s *Service) RequestGradeUpdateForDevice(deviceID string) error {

notification := model.NewIOSNotificationPayload(deviceID).Background(campusRequestToken.RequestID, model.IOSBackgroundCampusTokenRequest)

if _, err := s.Repository.SendBackgroundNotification(notification); err != nil {
if _, err := s.Repository.SendNotification(notification, model.IOSAPNSPushTypeBackground); err != nil {
log.WithError(err).Error("Could not send background notification")
return ErrCouldNotSendNotification
return errors.New("could not send notification")
}
return nil
}

func ValidateRequirementsForIOSNotificationsService() error {
if ApnsKeyId == "" {
return errors.New("APNS_KEY_ID env variable is not set")
for _, envVar := range []string{"APNS_KEY_ID", "APNS_TEAM_ID", "APNS_P8_FILE_PATH"} {
if os.Getenv(envVar) == "" {
return errors.New(envVar + " env variable is not set")
}
}

if ApnsTeamId == "" {
return errors.New("APNS_TEAM_ID env variable is not set")
}

if ApnsP8FilePath == "" {
return errors.New("APNS_P8_FILE_PATH env variable is not set")
}

if _, err := APNsEncryptionKeyFromFile(); err != nil {
if _, err := EncryptionKeyFromFile(); err != nil {
return errors.New("APNS P8 token is not valid or not set")
}

return nil
}

Expand Down
8 changes: 3 additions & 5 deletions server/backend/ios_notifications/crypto/encrypted_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,19 @@ func AsymmetricEncrypt(plaintext string, publicKey string) (*EncryptedString, er

func StringToPublicKey(pub string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pub))

if block == nil {
return nil, errors.New("failed to parse PEM block containing the public key")
}

key, err := x509.ParsePKIXPublicKey(block.Bytes)

if err != nil {
return nil, errors.New("failed to parse DER encoded public key: " + err.Error())
}

if pubKey, ok := key.(*rsa.PublicKey); ok {
return pubKey, nil
} else {
if pubKey, ok := key.(*rsa.PublicKey); !ok {
return nil, errors.New("failed to parse DER encoded public key: " + err.Error())
} else {
return pubKey, nil
}
}

Expand Down
Loading
Loading