Skip to content

Commit 628be6c

Browse files
committed
implement rate limiting
1 parent 6486f73 commit 628be6c

24 files changed

+799
-113
lines changed

android/app/src/main/java/com/httpsms/SmsManagerService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class SmsManagerService {
2121

2222
fun sendMessage(context: Context, message: Message, sentIntent:PendingIntent, deliveryIntent: PendingIntent) {
2323
getSmsManager(context)
24-
.sendTextMessage(message.contact, message.owner, message.content, sentIntent, deliveryIntent)
24+
.sendTextMessage(message.contact, null, message.content, sentIntent, deliveryIntent)
2525
}
2626

2727
@Suppress("DEPRECATION")

api/go.mod

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/NdoleStudio/http-sms-manager
33
go 1.18
44

55
require (
6+
cloud.google.com/go/cloudtasks v1.4.0
67
firebase.google.com/go v3.13.0+incompatible
78
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.8.3
89
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
@@ -24,18 +25,20 @@ require (
2425
go.opentelemetry.io/otel/exporters/jaeger v1.7.0
2526
go.opentelemetry.io/otel/sdk v1.7.0
2627
go.opentelemetry.io/otel/trace v1.7.0
27-
google.golang.org/api v0.74.0
28+
google.golang.org/api v0.85.0
29+
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad
30+
google.golang.org/protobuf v1.28.0
2831
gorm.io/datatypes v1.0.6
2932
gorm.io/driver/postgres v1.3.7
3033
gorm.io/gorm v1.23.5
3134
)
3235

3336
require (
34-
cloud.google.com/go v0.100.2 // indirect
35-
cloud.google.com/go/compute v1.5.0 // indirect
37+
cloud.google.com/go v0.102.0 // indirect
38+
cloud.google.com/go/compute v1.7.0 // indirect
3639
cloud.google.com/go/firestore v1.6.1 // indirect
37-
cloud.google.com/go/iam v0.1.1 // indirect
38-
cloud.google.com/go/storage v1.21.0 // indirect
40+
cloud.google.com/go/iam v0.3.0 // indirect
41+
cloud.google.com/go/storage v1.22.1 // indirect
3942
cloud.google.com/go/trace v1.2.0 // indirect
4043
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.32.3 // indirect
4144
github.com/KyleBanks/depth v1.2.1 // indirect
@@ -51,8 +54,10 @@ require (
5154
github.com/go-sql-driver/mysql v1.6.0 // indirect
5255
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
5356
github.com/golang/protobuf v1.5.2 // indirect
54-
github.com/google/go-cmp v0.5.7 // indirect
55-
github.com/googleapis/gax-go/v2 v2.2.0 // indirect
57+
github.com/google/go-cmp v0.5.8 // indirect
58+
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
59+
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
60+
github.com/googleapis/go-type-adapters v1.0.0 // indirect
5661
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
5762
github.com/jackc/pgconn v1.12.1 // indirect
5863
github.com/jackc/pgio v1.0.0 // indirect
@@ -83,16 +88,14 @@ require (
8388
go.uber.org/zap v1.13.0 // indirect
8489
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect
8590
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
86-
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
87-
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
88-
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
91+
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 // indirect
92+
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb // indirect
93+
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
8994
golang.org/x/text v0.3.7 // indirect
9095
golang.org/x/tools v0.1.10 // indirect
91-
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
96+
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
9297
google.golang.org/appengine v1.6.7 // indirect
93-
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
94-
google.golang.org/grpc v1.45.0 // indirect
95-
google.golang.org/protobuf v1.28.0 // indirect
98+
google.golang.org/grpc v1.47.0 // indirect
9699
gopkg.in/yaml.v2 v2.4.0 // indirect
97100
gorm.io/driver/mysql v1.3.2 // indirect
98101
)

api/go.sum

Lines changed: 66 additions & 29 deletions
Large diffs are not rendered by default.

api/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
//
2424
// @securitydefinitions.apikey ApiKeyAuth
2525
// @in header
26-
// @name X-API-Key
26+
// @name x-api-Key
2727
func main() {
2828
if len(os.Args) == 1 {
2929
di.LoadEnv()

api/pkg/di/container.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strconv"
88
"time"
99

10+
cloudtasks "cloud.google.com/go/cloudtasks/apiv2"
11+
1012
"go.opentelemetry.io/otel"
1113
"go.opentelemetry.io/otel/attribute"
1214
"go.opentelemetry.io/otel/propagation"
@@ -75,6 +77,8 @@ func NewContainer(projectID string) (container *Container) {
7577

7678
container.RegisterPhoneRoutes()
7779

80+
container.RegisterEventRoutes()
81+
7882
container.RegisterNotificationListeners()
7983

8084
// this has to be last since it registers the /* route
@@ -188,6 +192,10 @@ func (container *Container) DB() (db *gorm.DB) {
188192
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Phone{})))
189193
}
190194

195+
if err = db.AutoMigrate(&entities.PhoneNotification{}); err != nil {
196+
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.PhoneNotification{})))
197+
}
198+
191199
return container.db
192200
}
193201

@@ -213,6 +221,42 @@ func (container *Container) FirebaseAuthClient() (client *auth.Client) {
213221
return authClient
214222
}
215223

224+
// CloudTasksClient creates a new instance of cloudtasks.Client
225+
func (container *Container) CloudTasksClient() (client *cloudtasks.Client) {
226+
container.logger.Debug(fmt.Sprintf("creating %T", client))
227+
228+
client, err := cloudtasks.NewClient(context.Background(), option.WithCredentialsJSON(container.FirebaseCredentials()))
229+
if err != nil {
230+
container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize cloud tasks client"))
231+
}
232+
233+
return client
234+
}
235+
236+
// EventsQueueConfiguration creates a new instance of services.PushQueueConfig
237+
func (container *Container) EventsQueueConfiguration() (config services.PushQueueConfig) {
238+
container.logger.Debug(fmt.Sprintf("creating %T", config))
239+
240+
return services.PushQueueConfig{
241+
UserAPIKey: os.Getenv("EVENTS_QUEUE_USER_API_KEY"),
242+
Name: os.Getenv("EVENTS_QUEUE_NAME"),
243+
UserID: entities.UserID(os.Getenv("EVENTS_QUEUE_USER_ID")),
244+
ConsumerEndpoint: os.Getenv("EVENTS_QUEUE_ENDPOINT"),
245+
}
246+
}
247+
248+
// EventsQueue creates a new instance of services.PushQueue
249+
func (container *Container) EventsQueue() (queue services.PushQueue) {
250+
container.logger.Debug("creating events services.PushQueue")
251+
252+
return services.NewGooglePushQueue(
253+
container.Logger(),
254+
container.Tracer(),
255+
container.CloudTasksClient(),
256+
container.EventsQueueConfiguration(),
257+
)
258+
}
259+
216260
// FirebaseMessagingClient creates a new instance of messaging.Client
217261
func (container *Container) FirebaseMessagingClient() (client *messaging.Client) {
218262
container.logger.Debug(fmt.Sprintf("creating %T", client))
@@ -317,6 +361,8 @@ func (container *Container) EventDispatcher() (dispatcher *services.EventDispatc
317361
container.Logger(),
318362
container.Tracer(),
319363
container.EventRepository(),
364+
container.EventsQueue(),
365+
container.EventsQueueConfiguration(),
320366
)
321367

322368
container.eventDispatcher = dispatcher
@@ -343,6 +389,16 @@ func (container *Container) PhoneRepository() (repository repositories.PhoneRepo
343389
)
344390
}
345391

392+
// PhoneNotificationRepository creates a new instance of repositories.PhoneNotificationRepository
393+
func (container *Container) PhoneNotificationRepository() (repository repositories.PhoneNotificationRepository) {
394+
container.logger.Debug("creating GORM repositories.PhoneNotificationRepository")
395+
return repositories.NewGormPhoneNotificationRepository(
396+
container.Logger(),
397+
container.Tracer(),
398+
container.DB(),
399+
)
400+
}
401+
346402
// MessageThreadRepository creates a new instance of repositories.MessageThreadRepository
347403
func (container *Container) MessageThreadRepository() (repository repositories.MessageThreadRepository) {
348404
container.logger.Debug("creating GORM repositories.MessageThreadRepository")
@@ -446,6 +502,18 @@ func (container *Container) PhoneHandler() (handler *handlers.PhoneHandler) {
446502
)
447503
}
448504

505+
// EventsHandler creates a new instance of handlers.EventsHandler
506+
func (container *Container) EventsHandler() (handler *handlers.EventsHandler) {
507+
container.logger.Debug(fmt.Sprintf("creating %T", handler))
508+
509+
return handlers.NewEventsHandler(
510+
container.Logger(),
511+
container.Tracer(),
512+
container.EventsQueueConfiguration(),
513+
container.EventDispatcher(),
514+
)
515+
}
516+
449517
// RegisterMessageListeners registers event listeners for listeners.MessageListener
450518
func (container *Container) RegisterMessageListeners() {
451519
container.logger.Debug(fmt.Sprintf("registering listners for %T", listeners.MessageListener{}))
@@ -523,6 +591,8 @@ func (container *Container) NotificationService() (service *services.Notificatio
523591
container.Tracer(),
524592
container.FirebaseMessagingClient(),
525593
container.PhoneRepository(),
594+
container.PhoneNotificationRepository(),
595+
container.EventDispatcher(),
526596
)
527597
}
528598

@@ -556,6 +626,12 @@ func (container *Container) RegisterUserRoutes() {
556626
container.UserHandler().RegisterRoutes(container.AuthRouter())
557627
}
558628

629+
// RegisterEventRoutes registers routes for the /events prefix
630+
func (container *Container) RegisterEventRoutes() {
631+
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.EventsHandler{}))
632+
container.EventsHandler().RegisterRoutes(container.AuthRouter())
633+
}
634+
559635
// RegisterSwaggerRoutes registers routes for swagger
560636
func (container *Container) RegisterSwaggerRoutes() {
561637
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.MessageHandler{}))

api/pkg/entities/Phone.go

Lines changed: 0 additions & 17 deletions
This file was deleted.

api/pkg/entities/phone.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// Phone represents an android phone which has installed the http sms app
10+
type Phone struct {
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
13+
FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
14+
PhoneNumber string `json:"phone_number" example:"+18005550199"`
15+
MessagesPerMinute uint `json:"messages_per_second" example:"1"`
16+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
17+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
18+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
const (
10+
// PhoneNotificationStatusPending is the status when a notification is scheduled to be sent
11+
PhoneNotificationStatusPending = "pending"
12+
// PhoneNotificationStatusSent is the status when a notification has been sent
13+
PhoneNotificationStatusSent = "sent"
14+
// PhoneNotificationStatusFailed is the status when a notification could not be sent.
15+
PhoneNotificationStatusFailed = "failed"
16+
)
17+
18+
// PhoneNotificationStatus is the status of a phone notification
19+
type PhoneNotificationStatus string
20+
21+
// PhoneNotification represents an FCM notification to a mobile phone
22+
type PhoneNotification struct {
23+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;"`
24+
MessageID uuid.UUID `json:"message_id"`
25+
UserID UserID `json:"user_id"`
26+
PhoneID uuid.UUID `json:"phone_id"`
27+
Status string `json:"status"`
28+
ScheduledAt time.Time `json:"scheduled_at"`
29+
CreatedAt time.Time `json:"created_at"`
30+
UpdatedAt time.Time `json:"updated_at"`
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
7+
8+
"github.com/google/uuid"
9+
)
10+
11+
// EventTypeMessageNotificationScheduled is emitted when a new message notification is scheduled
12+
const EventTypeMessageNotificationScheduled = "message.notification.scheduled"
13+
14+
// MessageNotificationScheduledPayload is the payload of the MessageNotificationScheduledPayload event
15+
type MessageNotificationScheduledPayload struct {
16+
MessageID uuid.UUID `json:"id"`
17+
UserID entities.UserID `json:"user_id"`
18+
PhoneID uuid.UUID `json:"phone_id"`
19+
ScheduledAt time.Time `json:"scheduled_at"`
20+
NotificationID uuid.UUID `json:"notification_id"`
21+
}

api/pkg/handlers/events_handler.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
cloudevents "github.com/cloudevents/sdk-go/v2"
7+
8+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
10+
"github.com/davecgh/go-spew/spew"
11+
"github.com/gofiber/fiber/v2"
12+
"github.com/palantir/stacktrace"
13+
)
14+
15+
// EventsHandler handles heartbeat http requests.
16+
type EventsHandler struct {
17+
handler
18+
logger telemetry.Logger
19+
tracer telemetry.Tracer
20+
queueConfig services.PushQueueConfig
21+
service *services.EventDispatcher
22+
}
23+
24+
// NewEventsHandler creates a new EventsHandler
25+
func NewEventsHandler(
26+
logger telemetry.Logger,
27+
tracer telemetry.Tracer,
28+
queueConfig services.PushQueueConfig,
29+
service *services.EventDispatcher,
30+
) (h *EventsHandler) {
31+
return &EventsHandler{
32+
logger: logger.WithService(fmt.Sprintf("%T", h)),
33+
tracer: tracer,
34+
queueConfig: queueConfig,
35+
service: service,
36+
}
37+
}
38+
39+
// RegisterRoutes registers the routes for the MessageHandler
40+
func (h *EventsHandler) RegisterRoutes(router fiber.Router) {
41+
router.Post("/events", h.Dispatch)
42+
}
43+
44+
// Dispatch a cloud event
45+
// This is an internal API so no documentation provided
46+
func (h *EventsHandler) Dispatch(c *fiber.Ctx) error {
47+
ctx, span := h.tracer.StartFromFiberCtx(c)
48+
defer span.End()
49+
50+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
51+
52+
var request cloudevents.Event
53+
if err := c.BodyParser(&request); err != nil {
54+
msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
55+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
56+
return h.responseBadRequest(c, err)
57+
}
58+
59+
if err := request.Validate(); err != nil {
60+
msg := fmt.Sprintf("validation errors [%s], while dispatching event [%+#v]", spew.Sdump(err.Error()), request)
61+
ctxLogger.Warn(stacktrace.NewError(msg))
62+
return h.responseUnprocessableEntity(c, map[string][]string{"event": {err.Error()}}, "validation errors while dispatching event")
63+
}
64+
65+
if h.userIDFomContext(c) != h.queueConfig.UserID {
66+
msg := fmt.Sprintf("user with ID [%s], cannot dispatch event [%+#v]", h.userIDFomContext(c), request)
67+
ctxLogger.Error(stacktrace.NewError(msg))
68+
return h.responseForbidden(c)
69+
}
70+
71+
err := h.service.Dispatch(ctx, request)
72+
if err != nil {
73+
msg := fmt.Sprintf("cannot dispatch event with ID [%s]", request.ID())
74+
ctxLogger.Error(stacktrace.Propagate(err, msg))
75+
return h.responseInternalServerError(c)
76+
}
77+
78+
return h.responseNoContent(c, "event dispatched successfully")
79+
}

0 commit comments

Comments
 (0)