Skip to content

Commit 9edc72c

Browse files
authored
Merge pull request #1083 from ayeshLK/main
Introduce support for subscription auto-verification in websubhub
2 parents 86e9694 + 9da9571 commit 9edc72c

File tree

21 files changed

+495
-58
lines changed

21 files changed

+495
-58
lines changed

ballerina/Dependencies.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ dependencies = [
7373
[[package]]
7474
org = "ballerina"
7575
name = "http"
76-
version = "2.13.2"
76+
version = "2.13.3"
7777
dependencies = [
7878
{org = "ballerina", name = "auth"},
7979
{org = "ballerina", name = "cache"},

ballerina/annotation.bal

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
# Configuration for a WebSub Hub service.
1818
#
19-
# + leaseSeconds - The period for which the subscription is expected to be active in the `hub`
20-
# + webHookConfig - HTTP client configurations for subscription/unsubscription intent verification
19+
# + leaseSeconds - The period for which the subscription is expected to be active in the `hub`
20+
# + webHookConfig - HTTP client configurations for subscription/unsubscription intent verification
21+
# + autoVerifySubscriptionIntent - Configuration to enable or disable automatic subscription intent verification
2122
public type ServiceConfiguration record {|
2223
int leaseSeconds?;
2324
ClientConfiguration webHookConfig?;
25+
boolean autoVerifySubscriptionIntent = false;
2426
|};
2527

2628
# WebSub Hub Configuration for the service.

ballerina/commons.bal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const int LISTENER_START_ERROR = -3;
5454
const int LISTENER_DETACH_ERROR = -4;
5555
const int LISTENER_STOP_ERROR = -5;
5656
const int CLIENT_INIT_ERROR = -10;
57+
const SUB_AUTO_VERIFY_ERROR = -11;
58+
59+
const DEFAULT_HUB_LEASE_SECONDS = 86400;
5760

5861
# Options to compress using Gzip or deflate.
5962
#

ballerina/http_service.bal

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ isolated service class HttpService {
2626
private final int defaultLeaseSeconds;
2727
private final SubscriptionHandler subscriptionHandler;
2828

29-
isolated function init(HttpToWebsubhubAdaptor adaptor, string hubUrl, int leaseSeconds,
30-
*ClientConfiguration clientConfig) {
29+
isolated function init(HttpToWebsubhubAdaptor adaptor, string hubUrl, ServiceConfiguration? serviceConfig) {
3130
self.adaptor = adaptor;
3231
self.hub = hubUrl;
33-
self.defaultLeaseSeconds = leaseSeconds;
34-
self.subscriptionHandler = new (adaptor, clientConfig);
32+
self.defaultLeaseSeconds = serviceConfig?.leaseSeconds ?: DEFAULT_HUB_LEASE_SECONDS;
33+
ClientConfiguration clientConfig = serviceConfig?.webHookConfig ?: {};
34+
boolean autoVerifySubscriptionIntent = serviceConfig?.autoVerifySubscriptionIntent ?: false;
35+
self.subscriptionHandler = new (adaptor, autoVerifySubscriptionIntent, clientConfig);
3536
}
3637

3738
isolated resource function post .(http:Caller caller, http:Request request, http:Headers headers) returns Error? {

ballerina/hub_controller.bal

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.com).
2+
//
3+
// WSO2 LLC. licenses this file to you under the Apache License,
4+
// Version 2.0 (the "License"); you may not use this file except
5+
// in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing,
11+
// software distributed under the License is distributed on an
12+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13+
// KIND, either express or implied. See the License for the
14+
// specific language governing permissions and limitations
15+
// under the License.
16+
17+
# Component which can use to change the default subcription intent verification flow.
18+
public isolated class Controller {
19+
private final boolean autoVerifySubscriptionIntent;
20+
21+
private final map<Subscription|Unsubscription> autoVerifyState = {};
22+
23+
isolated function init(boolean autoVerifySubscriptionIntent) {
24+
self.autoVerifySubscriptionIntent = autoVerifySubscriptionIntent;
25+
}
26+
27+
# Marks a particular subscription as verified.
28+
#
29+
# + subscription - The `websubhub:Subscription` or `websubhub:Unsubscription` message
30+
# + return - A `websubhub:Error` if the `websubhub:Service` has not enabled subscription auto-verification,
31+
# or else nil
32+
public isolated function markAsVerified(Subscription|Unsubscription subscription) returns Error? {
33+
if !self.autoVerifySubscriptionIntent {
34+
return error Error(
35+
"Trying mark a subcription as verified, but the `hub` has not enabled automatic subscription intent verification",
36+
statusCode = SUB_AUTO_VERIFY_ERROR);
37+
}
38+
39+
string 'key = constructSubscriptionKey(subscription);
40+
lock {
41+
self.autoVerifyState['key] = subscription.cloneReadOnly();
42+
}
43+
}
44+
45+
isolated function skipSubscriptionVerification(Subscription|Unsubscription subscription) returns boolean {
46+
string 'key = constructSubscriptionKey(subscription);
47+
Subscription|Unsubscription? skipped;
48+
lock {
49+
skipped = self.autoVerifyState.removeIfHasKey('key).cloneReadOnly();
50+
}
51+
return skipped !is ();
52+
}
53+
}
54+
55+
isolated function constructSubscriptionKey(record {} message) returns string {
56+
string[] values = message.toArray().'map(v => string `${v.toString()}`);
57+
return string:'join(":::", ...values);
58+
}

ballerina/hub_listener.bal

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import ballerina/log;
1919

2020
# Represents a Service listener endpoint.
2121
public class Listener {
22-
private final int defaultHubLeaseSeconds = 864000;
2322
private http:Listener httpListener;
2423
private http:InferredListenerConfiguration listenerConfig;
2524
private int port;
@@ -65,18 +64,9 @@ public class Listener {
6564

6665
string hubUrl = self.retrieveHubUrl(name);
6766
ServiceConfiguration? configuration = retrieveServiceAnnotations('service);
68-
HttpToWebsubhubAdaptor adaptor = new('service);
69-
if configuration is ServiceConfiguration {
70-
int leaseSeconds = configuration?.leaseSeconds is int ? <int>(configuration?.leaseSeconds) : self.defaultHubLeaseSeconds;
71-
if configuration?.webHookConfig is ClientConfiguration {
72-
self.httpService = new(adaptor, hubUrl, leaseSeconds, <ClientConfiguration>(configuration?.webHookConfig));
73-
} else {
74-
self.httpService = new(adaptor, hubUrl, leaseSeconds);
75-
}
76-
} else {
77-
self.httpService = new(adaptor, hubUrl, self.defaultHubLeaseSeconds);
78-
}
79-
error? result = self.httpListener.attach(<HttpService> self.httpService, name);
67+
HttpToWebsubhubAdaptor adaptor = new ('service);
68+
self.httpService = new (adaptor, hubUrl, configuration);
69+
error? result = self.httpListener.attach(<HttpService>self.httpService, name);
8070
if (result is error) {
8171
return error Error("Error occurred while attaching the service", result, statusCode = LISTENER_ATTACH_ERROR);
8272
}
@@ -92,9 +82,7 @@ public class Listener {
9282
isolated function retrieveHubUrl(string[]|string? servicePath) returns string {
9383
string host = self.listenerConfig.host;
9484
string protocol = self.listenerConfig.secureSocket is () ? "http" : "https";
95-
96-
string concatenatedServicePath = "";
97-
85+
string concatenatedServicePath = "";
9886
if servicePath is string {
9987
concatenatedServicePath += "/" + <string>servicePath;
10088
} else if servicePath is string[] {

ballerina/natives.bal

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ isolated class HttpToWebsubhubAdaptor {
4545
'class: "io.ballerina.stdlib.websubhub.NativeHttpToWebsubhubAdaptor"
4646
} external;
4747

48-
isolated function callOnSubscriptionMethod(Subscription msg, http:Headers headers) returns SubscriptionAccepted|
49-
SubscriptionPermanentRedirect|SubscriptionTemporaryRedirect|BadSubscriptionError|InternalSubscriptionError|error = @java:Method {
48+
isolated function callOnSubscriptionMethod(Subscription msg, http:Headers headers, Controller hubController)
49+
returns SubscriptionAccepted|SubscriptionPermanentRedirect|SubscriptionTemporaryRedirect|BadSubscriptionError
50+
|InternalSubscriptionError|error = @java:Method {
5051
'class: "io.ballerina.stdlib.websubhub.NativeHttpToWebsubhubAdaptor"
5152
} external;
5253

@@ -59,7 +60,7 @@ isolated class HttpToWebsubhubAdaptor {
5960
'class: "io.ballerina.stdlib.websubhub.NativeHttpToWebsubhubAdaptor"
6061
} external;
6162

62-
isolated function callOnUnsubscriptionMethod(Unsubscription msg, http:Headers headers)
63+
isolated function callOnUnsubscriptionMethod(Unsubscription msg, http:Headers headers, Controller hubController)
6364
returns UnsubscriptionAccepted|BadUnsubscriptionError|InternalUnsubscriptionError|error = @java:Method {
6465
'class: "io.ballerina.stdlib.websubhub.NativeHttpToWebsubhubAdaptor"
6566
} external;

ballerina/subscription.bal

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ import ballerina/uuid;
1919

2020
isolated class SubscriptionHandler {
2121
private final HttpToWebsubhubAdaptor adaptor;
22+
private final Controller hubController;
2223
private final readonly & ClientConfiguration clientConfig;
2324

2425
private final boolean isOnSubscriptionAvailable;
2526
private final boolean isOnSubscriptionValidationAvailable;
2627
private final boolean isOnUnsubscriptionAvailable;
2728
private final boolean isOnUnsubscriptionValidationAvailable;
2829

29-
isolated function init(HttpToWebsubhubAdaptor adaptor, *ClientConfiguration clientConfig) {
30+
isolated function init(HttpToWebsubhubAdaptor adaptor, boolean autoVerifySubscriptionIntent,
31+
ClientConfiguration clientConfig) {
3032
self.adaptor = adaptor;
33+
self.hubController = new (autoVerifySubscriptionIntent);
3134
self.clientConfig = clientConfig.cloneReadOnly();
3235
string[] methodNames = adaptor.getServiceMethodNames();
3336
self.isOnSubscriptionAvailable = methodNames.indexOf("onSubscription") is int;
@@ -43,7 +46,8 @@ isolated class SubscriptionHandler {
4346
return response;
4447
}
4548

46-
SubscriptionAccepted|Redirect|error result = self.adaptor.callOnSubscriptionMethod(message, headers);
49+
SubscriptionAccepted|Redirect|error result = self.adaptor.callOnSubscriptionMethod(
50+
message, headers, self.hubController);
4751
if result is Redirect {
4852
return result;
4953
}
@@ -63,17 +67,20 @@ isolated class SubscriptionHandler {
6367
return;
6468
}
6569

66-
string challenge = uuid:createType4AsString();
67-
[string, string?][] params = [
68-
[HUB_MODE, MODE_SUBSCRIBE],
69-
[HUB_TOPIC, message.hubTopic],
70-
[HUB_CHALLENGE, challenge],
71-
[HUB_LEASE_SECONDS, message.hubLeaseSeconds]
72-
];
73-
http:Response subscriberResponse = check sendNotification(message.hubCallback, params, self.clientConfig);
74-
string responsePayload = check subscriberResponse.getTextPayload();
75-
if challenge != responsePayload {
76-
return;
70+
boolean skipIntentVerification = self.hubController.skipSubscriptionVerification(message);
71+
if !skipIntentVerification {
72+
string challenge = uuid:createType4AsString();
73+
[string, string?][] params = [
74+
[HUB_MODE, MODE_SUBSCRIBE],
75+
[HUB_TOPIC, message.hubTopic],
76+
[HUB_CHALLENGE, challenge],
77+
[HUB_LEASE_SECONDS, message.hubLeaseSeconds]
78+
];
79+
http:Response subscriberResponse = check sendNotification(message.hubCallback, params, self.clientConfig);
80+
string responsePayload = check subscriberResponse.getTextPayload();
81+
if challenge != responsePayload {
82+
return;
83+
}
7784
}
7885

7986
VerifiedSubscription verifiedSubscription = {
@@ -100,7 +107,8 @@ isolated class SubscriptionHandler {
100107
return response;
101108
}
102109

103-
UnsubscriptionAccepted|error result = self.adaptor.callOnUnsubscriptionMethod(message, headers);
110+
UnsubscriptionAccepted|error result = self.adaptor.callOnUnsubscriptionMethod(
111+
message, headers, self.hubController);
104112
return processOnUnsubscriptionResult(result);
105113
}
106114

@@ -115,16 +123,19 @@ isolated class SubscriptionHandler {
115123
_ = check sendNotification(message.hubCallback, params, self.clientConfig);
116124
}
117125

118-
string challenge = uuid:createType4AsString();
119-
[string, string?][] params = [
120-
[HUB_MODE, MODE_UNSUBSCRIBE],
121-
[HUB_TOPIC, message.hubTopic],
122-
[HUB_CHALLENGE, challenge]
123-
];
124-
http:Response subscriberResponse = check sendNotification(message.hubCallback, params, self.clientConfig);
125-
string responsePayload = check subscriberResponse.getTextPayload();
126-
if challenge != responsePayload {
127-
return;
126+
boolean skipIntentVerification = self.hubController.skipSubscriptionVerification(message);
127+
if !skipIntentVerification {
128+
string challenge = uuid:createType4AsString();
129+
[string, string?][] params = [
130+
[HUB_MODE, MODE_UNSUBSCRIBE],
131+
[HUB_TOPIC, message.hubTopic],
132+
[HUB_CHALLENGE, challenge]
133+
];
134+
http:Response subscriberResponse = check sendNotification(message.hubCallback, params, self.clientConfig);
135+
string responsePayload = check subscriberResponse.getTextPayload();
136+
if challenge != responsePayload {
137+
return;
138+
}
128139
}
129140

130141
VerifiedUnsubscription verifiedUnsubscription = {

0 commit comments

Comments
 (0)