Description
This is mostly for internal use, but it allows to integrate with the Nashpush service in a way that hides the fact that it is being used. This can be useful for white-labeling or when you want to keep the underlying technology hidden from the end users.
Requests
In order to prevent manual service identification, Nashpush supports different versions of API with unique structures. Each structure set requires manual client script integration, which is not a huge problem, but may require some time to do. As for the server-side, it is possible to use the same API for all versions with different adapters using AdaptativeSerializer from webpush-python
In order to obtain a unique request-response communication that is not easily identifiable as Nashpush, the following approaches are used:
- requests are made to different URLs with unique headers
- request bodies are structured in a way that does not resemble Nashpush’s typical structure (by using
AdaptativeSerializerfrom webpush-python) - responses are also structured in a way that does not resemble Nashpush’s typical structure (if needed)
Subscription
Client
- update code with new payload structure
- use different key for
Channel-Tokenin the request headers
** Safari **
fetch("{{ subscribe_url }}", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
from_url: window.location.href,
browser_language: browserLanguage,
tags: that.tags,
preconditions: preconditions,
token: deviceToken,
apns_app: that.base.apns_uuid,
window_id: that.base.window ? that.base.window.window_id : null,
window_pool_id: that.base.window ? that.base.window.pool_id : null,
with_tracing: that.base.withTracing,
}),
})** Firebase **
fetch("{{ subscribe_url }}", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
from_url: window.location.href,
browser_language: browserLanguage,
tags: that.tags,
subscription: {
auth: subscription_data["keys"]["auth"],
p256dh: subscription_data["keys"]["p256dh"],
endpoint: subscription_data["endpoint"],
installation_id: installation_id,
},
firebase_app: that.base.firebase_uuid,
firebase_id: that.base.firebase_id,
window_id: that.base.window ? that.base.window.window_id : null,
window_pool_id: that.base.window ? that.base.window.pool_id : null,
pwa: that.base.isPWA(),
os: ua.platform,
os_version: ua.platformVersion,
}),
}Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- use
AdaptativeSerializerto structure the request body in a way that does not resemble Nashpush’s typical structure
Code snippet
Client
- update code with new response data
- use different key for
Channel-Tokenin the request headers
let url = "{{ code_snippet_url }}";
fetch(url, {
headers: {
"Channel-Token": that.channel_id,
},
credentials: "include",
}).then((response) => {
let w;
if (response.status === 200) {
response.json().then((result) => {
that.apns_uuid = result["apns_app"]["uuid"];
that.firebase_id = result["firebase_app"]["firebase_id"];
that.firebase_uuid = result["firebase_app"]["uuid"];
that.firebase_vapid_key_array =
result["firebase_app"]["vapid_key_array"];
that.firebase_public_key = result["firebase_app"]["public_key"];
that.firebase_config = result["firebase_app"]["config"];
that.subscription_attempts_count =
result["subscription_attempts_count"];
that.trigger_selector = result["trigger_selector"];
w = result["subscription_window"];
if (w) {
that.window = new window.subscriptionWindow(
w["id"],
w["title"],
w["description"],
w["image_url"],
w["confirm_button_text"],
w["cancel_button_text"],
w["appearance_event"],
w["appearance_timeout"],
w["pool"],
w["styles"]
);
resolve();
} else {
resolve();
}
});
} else {
reject();
}
});Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- response data needs to be structured in a way that does not resemble Nashpush’s typical structure
Session rules
Client
- update code to work with a new payload structure
- use different key for
Channel-Tokenin the request headers
fetch("{{ add_session_rule_url }}", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
window: that.window.window_id,
name: "allow_notifications",
value_str: "1",
}),
})Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- use
AdaptativeSerializerto structure the request body in a unique way
Subscriber update
Client
- update code to work with a new payload structure
- use different key for
Channel-Tokenin the request headers
fetch("{{ subscriber_update_url }}", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
subscription: {
auth: subscription_data["keys"]["auth"],
p256dh: subscription_data["keys"]["p256dh"],
endpoint: subscription_data["endpoint"],
installation_id: installation_id,
},
token: token,
}),Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- use
AdaptativeSerializerto structure the request body in a unique way
Subscriber tagging
Client
- update code to work with a new payload structure
- use different key for
Channel-Tokenin the request headers
fetch("{{ subscribe_url }}" + this.uuid + "/tags/", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
key: key,
value: value,
}),Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- use
AdaptativeSerializerto structure the request body in a unique way
Channel events
Client
- update code to work with a new payload structure
- use different key for
Channel-Tokenin the request headers
return fetch("{{ channels_events_url }}", {
method: "POST",
headers: {
"Channel-Token": this.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
event: event,
subscriber_uuid: this.uuid,
subscribed_url: window.location.href,
is_system: true,
}),
})Server
- replace
Channel-Tokenwith something else - use unique URL for the request
- use
AdaptativeSerializerto structure the request body in a unique way
Set source param
Client
- use different key for
Channel-Tokenin the request headers
fetch("{{ subscribe_url }}" + this.uuid + "/params/", {
method: "POST",
headers: {
"Channel-Token": that.channel_id,
"Content-Type": "application/json;charset=UTF-8",
},
credentials: "include",
body: JSON.stringify({
key: key,
value: value,
}),
})Server
- replace
Channel-Tokenwith something else - use unique URL for the request
Safari config
Server
- replace
Channel-Tokenwith something else - use unique URL for the request
Subscribers callbacks
Client
return fetch('{{ subscribers_url }}' + meta['subscriber_id'] + '/callbacks/', {
method: 'POST',
body: JSON.stringify({
'type': 'message_awake',
'token': meta['subscriber_token'],
'data': { 'message_id': meta['message_id'] }
}),
headers: {
'content-type': 'text/plain',
}
}).then(() => {Server
- replace
Channel-Tokenwith something else - use unique URL for the request
Static files
After being updated with the new structure, static files are obfuscated and served from the unique static endpoint. A list of files:
init.js- the main script that initializes the service worker and handles subscriptionservice-worker.js- the service worker script that handles push notifications and background syncworker.js- just needs a new endpoint to grab the worker script from
Urls mapping
The following URLs map is supposed to be configured on the web server to redirect requests to standard Nashpush API endpoints.
{
// directory
'/api/refresh/': '/api/v2/subscribers/refresh/',
'/api/subscribers/': '/api/v2/subscribers/',
'/api/subscribers/ID/callbacks/': '/api/v2/subscribers/ID/callbacks/',
'/api/safari/': '/api/v1/safari-configs/',
// manager
'/api/snippet/': '/api/v2/code-snippet/',
'/api/rules/': '/api/v2/window-session-rules/',
'/api/window-block/': '/api/v2/window-session-rules/?name=allow_notifications&value=0',
'/api/events/': '/api/v2/channels/events/',
'/api/start/': '/api/v2/js-code-snippet/',
// static
'/static/service_worker.js': '/mng/channels/v2/service_worker.js',
'/static/sdk.js': '/mng/channels/v2/sdk.js',
}How to use
In order to start using the hidden mode, you just need to setup a proper code snippet and service worker from any of known hidden domains.
Here is an example of how to do it for a channel without custom subscription window using hide-domain.syspunk.org domain:
Code snippet
<script src="https://hide-domain.syspunk.org/static/sdk.js"></script>
<script>
window.subscribe("PTExMjI9Li89MzM1").start();
</script>Delayed (hidden) snippet
<script src="https://hide-domain.syspunk.org/static/sdk.js"></script>
<script>
let script = document.createElement('script');
script.type = 'module';
script.src = window.location.origin + `/api/start/?channel_token=PTM1Mzc0PS89NDA3MQ==`;
document.head.appendChild(script);
</script>Service worker
importScripts('https://hide-domain.syspunk.org/static/service_worker.js');