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 AdaptativeSerializer from 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-Token in 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-Token with something else
  • use unique URL for the request
  • use AdaptativeSerializer to 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-Token in 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-Token with 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-Token in 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-Token with something else
  • use unique URL for the request
  • use AdaptativeSerializer to 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-Token in 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-Token with something else
  • use unique URL for the request
  • use AdaptativeSerializer to 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-Token in 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-Token with something else
  • use unique URL for the request
  • use AdaptativeSerializer to 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-Token in 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-Token with something else
  • use unique URL for the request
  • use AdaptativeSerializer to structure the request body in a unique way

Set source param

Client

  • use different key for Channel-Token in 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-Token with something else
  • use unique URL for the request

Safari config

Server

  • replace Channel-Token with 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-Token with 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 subscription
  • service-worker.js - the service worker script that handles push notifications and background sync
  • worker.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');