Cloudflare worker strips headers - javascript

I'm trying to migrate my CORS checker app from heroku to cloudflare workers as it seems to be a perfect fit for my use case.
The heroku app uses axios to asks a website for an options call to see if there are any headers with x-frame-options. If so, I check if it's denied, and return this as a boolean value in the REST response.
This sounds really simple, but I'm having trouble making it work on cloudflare workers. Am I correct in assuming that cloudflare is stripping these headers and there is no way to get this working with cloudflare workers?
Here is the full worker code
// node_modules/itty-router/dist/itty-router.mjs
var e = ({ base: e2 = "", routes: r = [] } = {}) => ({ __proto__: new Proxy({}, { get: (a, o, t) => (a2, ...p) => r.push([o.toUpperCase(), RegExp(`^${(e2 + a2).replace(/(\/?)\*/g, "($1.*)?").replace(/(\/$)|((?<=\/)\/)/, "").replace(/(:(\w+)\+)/, "(?<$2>.*)").replace(/:(\w+)(\?)?(\.)?/g, "$2(?<$1>[^/]+)$2$3").replace(/\.(?=[\w(])/, "\\.").replace(/\)\.\?\(([^\[]+)\[\^/g, "?)\\.?($1(?<=\\.)[^\\.")}/*$`), p]) && t }), routes: r, async handle(e3, ...a) {
let o, t, p = new URL(e3.url), l = e3.query = {};
for (let [e4, r2] of p.searchParams)
l[e4] = void 0 === l[e4] ? r2 : [l[e4], r2].flat();
for (let [l2, s, c] of r)
if ((l2 === e3.method || "ALL" === l2) && (t = p.pathname.match(s))) {
e3.params = t.groups || {};
for (let r2 of c)
if (void 0 !== (o = await r2(e3.proxy || e3, ...a)))
return o;
}
} });
// src/index.ts
var router = e();
var corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Access-Control-Max-Age": "86400"
};
function jsonResponse(body) {
return new Response(JSON.stringify(body), {
status: 200,
headers: {
"Content-Type": "application/json",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload"
}
});
}
function handleOptions(request) {
let headers = request.headers;
if (headers.get("Origin") !== null && headers.get("Access-Control-Request-Method") !== null && headers.get("Access-Control-Request-Headers") !== null) {
let respHeaders = {
...corsHeaders,
"Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers")
};
return new Response(null, {
headers: respHeaders
});
} else {
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS"
}
});
}
}
router.options("/test-cors", async function(request) {
return handleOptions(request);
});
router.post("/test-cors", async function(request) {
const url = request.body.url;
const externalRequest = new Request(url, { method: "head" });
try {
let response = await fetch(externalRequest);
if (response.headers.get("x-frame-options") !== null) {
const frameOptionsValue = response.headers.get("x-frame-options");
if (frameOptionsValue === "SAMEORIGIN") {
return jsonResponse({ canAccess: false });
} else {
return jsonResponse({ canAccess: true });
}
} else {
return jsonResponse({ canAccess: true });
}
} catch (error) {
console.error(error);
if (error.code === "ENOTFOUND" || error.response.status === 404) {
return jsonResponse({ canAccess: false, reason: "NOT_FOUND" });
} else {
return jsonResponse({ canAccess: false });
}
}
});
router.all("*", (request, args) => {
return new Response("Not Found", {
status: 404,
headers: {
"Content-Type": "text/html; charset=utf-8",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload"
}
});
});
var src_default = {
async fetch(request, env, ctx) {
return router.handle(request);
}
};
export {
src_default as default
};
//# sourceMappingURL=index.js.map

So I've talked with the developers behind the feature and it turns out the website I was trying to test for the header had a different behavior set for the calling IP.
Since the editor runs fetch from your browser the IP was different and was not triggering that effect.

Related

Apollo Websocket - Unable to initiate with new connection with latest JWT token

I have a vue application that connects to a graphQL server using vue apollo. The vue-apollo.js code is as follows
const httpEndpoint = process.env.XXX;
const wsEndpoint = process.env.XXX;
let baseLink = new ApolloLink((operation, forward) => {
let isTokenExpired = isJwtExpired(token)
console.log('isExpired is:',isTokenExpired) ;
if (isTokenExpired) {
Auth.currentSession().then((data) => {
token = data.idToken.jwtToken
localStorage.setItem('JoT', JSON.stringify(token));
const headerData = token? {authorization: `Bearer ${token}`,}: {};
operation.setContext({headers: headerData})
return forward(operation)
})
} else {
token = JSON.parse(token);
const headerData = token? {authorization: `Bearer ${token}`,}: {};
operation.setContext({headers: headerData})
return forward(operation)
}
})
let observerLink = new ApolloLink((operation, forward) => {
return forward(operation)
})
const baseAndObserverLink = baseLink.concat(observerLink)
const errorLink = onError(error => {
console.log(JSON.stringify(error))
if (error.graphQLErrors) {
console.log("GraphQL Error detected")
if (error.graphQLErrors.extensions && error.graphQLErrors.extensions.code && error.graphQLErrors.extensions.code === 'invalid-jwt') {
console.log("JWT EXPIRED")
}
} else if (error.networkError && error.networkError.extensions && error.networkError.extensions.code && error.networkError.extensions.code === 'start-failed') {
console.log("GraphQL Error detected type 2")
console.log("Unable to Connect")
}else if (error.networkError && error.networkError.extensions && error.networkError.extensions.code && error.networkError.extensions.code === 'validation-failed') {
console.log("GraphQL Error detected type 3")
console.log("Validation Error")
}
else {
console.log(JSON.stringify(error))
}
})
var wsLink = new WebSocketLink({
uri: wsEndpoint,
options: {
reconnect: true,
lazy: true,
timeout: 30000,
connectionParams: async () => {
const token = JSON.parse(localStorage.getItem('JWT'));
return {
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
}
},
},
});
export var filesRoot =
process.env.VUE_APP_FILES_ROOT ||
httpEndpoint.substr(0, httpEndpoint.indexOf('/graphql'));
Vue.prototype.$filesRoot = filesRoot;
var httpLink = new HttpLink({
uri: httpEndpoint,
});
var splitLLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
export var defaultOptions = {
httpEndpoint,
wsEndpoint,
tokenName: 'JWT',
persisting: false,
websocketsOnly: true,
ssr: false,
link:baseAndObserverLink.concat(errorLink).concat(splitLLink),
};
export function createProvider(options = {}) {
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
});
apolloClient.wsClient = wsClient;
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
async errorHandler(error) {
},
});
return apolloProvider;
}
This code when we first login works well. When I refetch a new token the query part gets updated with new context and everything works fine. However the websocket connection seems to keep using the old token to reinitialise connection. So far I couldn't find any suitable solution except for do a forced reload which reinitilises the entire app and wss conection can work as we have latest token being passed. Is there another way

Angular Node server returns You provided 'undefined' where a stream was expected

I am using angular 9 + universal. No errors while i run ng serve , then I build the app with npm run build:ssr and try to run with node : node dist/app/server/main.js and get the following error in terminal :
Node Express server listening on http://localhost:4000 TypeError: You
provided 'undefined' where a stream was expected. You can provide an
Observable, Promise, Array, or Iterable.
at subscribeTo (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:2547459)
at subscribeToResult (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:2775326)
at CatchSubscriber.error (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1997435)
at Observable_Observable._trySubscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952954)
at Observable_Observable.subscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952574)
at CatchOperator.call (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1996823)
at Observable_Observable.subscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952428)
at _task (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1751796)
at Observable_Observable.Observable.a.observer [as _subscribe] (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1752141)
at Observable_Observable._trySubscribe (C:\Users\andri\OneDrive\Desktop\devfox\autorent\ng-videocms\dist\autorent\server\main.js:1:1952792)
As I've explored, my app does 2 api calls on start :
app.component.ts :
ngOnInit(){
get1();
get2();
}
get1() {
const loc = this.locationService.getPickupLocations().subscribe((data: Location[]) => {
this.pickupLocations = data;
this.formGroup.get(LocationFields.pickup).setValue(data[0].getId());
this.pickupLocationsList = this.pickupLocations.map((data): ISelectOption => {
return {
label: data.getName(),
value: data.getId(),
};
});
},
(error)=> {
console.log(error)
},
() => {
this.subs.add(loc);
this.pickupDateChange(this.formGroup.get(this.LocationFields.pickupDate).value);
});
}
get2() {
const drop = this.locationService.getDropOffLocations().subscribe((data: Location[]) => {
this.dropoffLocations = data;
this.formGroup.get(LocationFields.dropoff).setValue(data[1].getId());
this.dropoffLocationsList = this.dropoffLocations.map((data): ISelectOption => {
return {
label: data.getName(),
value: data.getId(),
};
});
},(error)=> {
console.log(error)
},
() => {
this.subs.add(drop);
});
}
LocationService.ts :
static locationsEndpoint = 'public/locations/rental';
getPickupLocations(): Observable<Location[]> {
const reqHeader = new HttpHeaders({ 'Content-Type': 'application/json', 'No-Auth': 'True' });
return this.http.get(`${LocationsService.locationsEndpoint}/pickup`, { headers: reqHeader }).pipe(
map((data: ILocationResponse) => this.hydrateCollectionData(data, LocationsHydrator))
);
}
getDropOffLocations(): Observable<Location[]> {
const reqHeader = new HttpHeaders({ 'Content-Type': 'application/json', 'No-Auth': 'True' });
return this.http.get(`${LocationsService.locationsEndpoint}/dropoff`, { headers: reqHeader }).pipe(
map((data: ILocationResponse) => this.hydrateCollectionData(data, LocationsHydrator))
);
}
And Interceptors :
private static BASE_URL = environment.apiUrl;
readonly HEADER_AUTHORIZATION = 'Authorization';
readonly HEADER_ACCEPT = 'Accept';
readonly HEADER_CONTENT_TYPE = 'Content-Type';
readonly ACCEPT_LANGUAGE = 'Accept-Language';
constructor(
private authService: AuthService,
private localeService: LocaleService
) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.get('skip')) {
return next.handle(req);
}
if (req.url.startsWith('./assets')) {
return next.handle(req);
}
req = req.clone({
url: this._prefixUrl(req.url)
});
req = req.clone({
headers: req.headers.set(this.HEADER_ACCEPT, 'application/json')
});
req = req.clone({
headers: req.headers.set(this.HEADER_CONTENT_TYPE, 'application/json')
});
req = req.clone({
headers: req.headers.set(this.ACCEPT_LANGUAGE, this.localeService.getLocale())
});
// Set token if exists
const token = this.authService.getToken();
if (token) {
req = req.clone({
headers: req.headers.set(this.HEADER_AUTHORIZATION, `Bearer ${token}`)
});
}
return next.handle(req).pipe(
catchError((httpErrorResponse: HttpErrorResponse) => {
if(httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
}
})
);
}
private _prefixUrl(path: string): string {
if (path.indexOf('/') === 0) {
path = path.substr(1, path.length - 1);
}
return `${Interceptor.BASE_URL}/${path}`;
}
I tried without these calls , tried to comment one of them.
When i disable those calls (comment them) , app works fine.
When i call them later, after i commented them, (onclick), app works fine.
When i disable interceptors , it works (SO the problem is in them, what to change ?? )
How to fix it ? And why it is happening ?
The trace seems to suggest the problem is related to CatchOperator.
I see only one place where catchError operator is used in your code
return next.handle(req).pipe(
catchError((httpErrorResponse: HttpErrorResponse) => {
if(httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
}
})
);
In your code you seem to assume that the function passed to catchError will receive always an instance of HttpErrorResponse which is not the case. catchError will be used for any error and so the function passed to it can receive any type of error.
What happens if httpErrorResponse.error is null but you still have an error in the upstream Observables somewhere? According to the code above you do not return anything, so this may be the reason why the log says You provided 'undefined' where a stream was expected.
Rather than not doing anything, you can throw an error, so that you should get the details of the error causing the fact that you enter catchError operator, so something like this
catchError(err => {
if(error instanceof HttpErrorResponse && httpErrorResponse.error !== undefined){
const customError: ApiErrors = {
name: httpErrorResponse.error.name,
message: httpErrorResponse.error.message,
errors: httpErrorResponse.error.errors
};
return throwError(customError);
} else {
throw err
}
})
);

Token generation working in IE (getting 200) but not Chrome and FF (getting 401)

I have some code that's supposed to generate a new token if certain conditions are met.
In IE the token generator returns a 200 (and console logs the token) but in Chrome and Firefox I'm getting 401 errors (console logging the token shows null).
I've been looking up reasons as to why this is, but for some reason most of the SO posts I've come across have issues with IE and tokens and not Chrome.
permissionToCallAPI = new Promise(function(resolve, reject) {
function sessionGet(key) {
let stringValue = window.sessionStorage.getItem(key);
if (stringValue !== null) {
try {
const { value, expirationDateStr } = JSON.parse(stringValue);
if (value && expirationDateStr) {
let expirationDate = new Date(value.expirationDateStr);
if (expirationDate > new Date()) {
return value;
}
isAuthorized(value)
.then(resp => {
console.log("Valid Token");
return value;
})
.catch(err => {
throw "Failed Authentication.";
});
}
} catch (e) {
console.log(e);
window.sessionStorage.removeItem(key);
return null;
}
}
return null;
} // sessionGet()
// add into session
function sessionSet(key, value, expirationInMin) {
if (!expirationInMin) {
expirationInMin = 59;
}
var expirationDate = new Date(
new Date().getTime() + 60000 * expirationInMin
);
var newValue = {
value: value,
expirationDate: expirationDate.toISOString()
};
window.sessionStorage.setItem(key, JSON.stringify(newValue));
}
let _token = sessionGet("tokenSet");
console.log(_token); // null in Chr (not IE)
if (_token == null) {
$.ajax({
url: _RestHost + "/User/GenerateToken",
method: "GET", // using POST gets an error
cache: false,
withCredentials: true,
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Authorization": _token
},
success: function(data) {
sessionSet("tokenSet", data.AuthToken);
resolve(data.AuthToken);
},
error: function(err) {
reject(err);
}
});
} else {
resolve(_token);
}
});

coverting javascript to python

I have a yale smart alarm and come across the the below javascript that allows you to access the alarm to get the status and set it. I'm wanting to use this in my home assistant set to which uses python.
const fetch = require('node-fetch');
const setCookie = require('set-cookie-parser');
const urls = {
login: 'https://www.yalehomesystem.co.uk/homeportal/api/login/check_login',
getStatus: 'https://www.yalehomesystem.co.uk/homeportal/api/panel/get_panel_mode',
setStatus: 'https://www.yalehomesystem.co.uk/homeportal/api/panel/set_panel_mode?area=1&mode=',
};
function getSessionCookie(username, password) {
let sessionCookie = null;
return fetch(urls.login, {
method: 'POST',
body: `id=${encodeURIComponent(username)}&password=${password}&rememberme=on&notify_id=&reg_id=Name`,
headers: {
'Accept': 'application/json, application/xml, text/plain, text/html, *.*',
'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'
},
})
.then((res) => {
sessionCookie = res.headers._headers['set-cookie'];
return res.json();
}).then(json => {
if (json.result === '0') {
return Promise.reject('Incorrect account details');
}
else {
return sessionCookie[0];
}
})
}
function getStatus(sessionCookie) {
return fetch(urls.getStatus, {
method: 'POST',
headers: {
'Cookie': sessionCookie,
},
}).then(res => res.text()).then(textResponse => {
// When initially writing this code I found if cookie payload
// was invalid I got this text response so I added this code to
// handle this, shouldn't happen but good to have an error message
// for this use case
if (textResponse === 'Disallowed Key Characters.') {
return Promise.reject('Invalid request');
}
else {
try {
// Hopefully if we got to this point we can parse the json
const json = JSON.parse(textResponse);
if (json.result === '0') {
return Promise.reject('Unable to get status');
}
else {
return json;
}
} catch (error) {
// If you get this error message I likely have not handled
// a error state that I wasnt aware of
return Promise.reject('Unable to parse response');
}
}
});
}
function setStatus (sessionCookie, mode) {
return new Promise((resolve, reject) => {
if (!sessionCookie || sessionCookie.length === 0) {
reject('Please call getSessionCookie to get your session cookie first');
}
if (mode !== 'arm' && mode !== 'home' && mode !== 'disarm') {
reject('Invalid mode passed to setStatus');
}
resolve(fetch(`${urls.setStatus}${mode}`, {
method: 'POST',
headers: {
'Cookie': sessionCookie,
},
}));
});
}
module.exports = {
getSessionCookie,
getStatus,
setStatus,
}
i'm every new to coding but was able to piece the below together to return the current status of my alarm. the problem is I'm unable to get it to work. based on the above code could someone please tell me what I'm missing, or if I'm going down the wrong rabbit hole....
import requests
import webbrowser
url = “https://www.yalehomesystem.co.uk/homeportal/api/login/check_login”
payload = {‘username’: ‘email#domaim.com’, ‘password’: ‘mypass’}
with requests.session() as s:
# fetch the login page
s.get(url, data=payload)
url1='https://www.yalehomesystem.co.uk/homeportal/api/panel/get_panel_mode'
# post to the login form
r = s.post(url1, data=payload)
print(r.text)
To add more contexts I'm getting the following error
{"result":"0","message":"system.permission_denied","code":"999"}

Web Pushnotification 'UnauthorizedRegistration' or 'Gone' or 'Unauthorized'- subscription expires

I have developed a push notification service for my web site. the service worker is:
'use strict';
self.addEventListener('push', function (event) {
var msg = {};
if (event.data) {
msg = event.data.json();
}
let notificationTitle = msg.title;
const notificationOptions = {
body: msg.body,//body
dir:'rtl',//direction
icon: msg.icon,//image
data: {
url: msg.url,//click
},
};
event.waitUntil(
Promise.all([
self.registration.showNotification(
notificationTitle, notificationOptions),
])
);
});
self.addEventListener('notificationclick', function (event) {
event.notification.close();
let clickResponsePromise = Promise.resolve();
if (event.notification.data && event.notification.data.url) {
clickResponsePromise = clients.openWindow(event.notification.data.url);
}
const fetchOptions =
{ method: 'post'};
fetch('http://localhost:5333/usrh.ashx?click=true', fetchOptions).
then(function (response)
{
if (response.status >= 400 && response.status < 500)
{
throw new Error('Failed to send push message via web push protocol');
}
}).catch((err) =>
{
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
self.addEventListener('notificationclose', function (event) {
const fetchOptions =
{ method: 'post'};
fetch('http://localhost:5333/usrh.ashx?close=true', fetchOptions).
then(function (response)
{
if (response.status >= 400 && response.status < 500)
{
throw new Error('Failed to send push message via web push protocol');
}
}).catch((err) =>
{
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
self.addEventListener('pushsubscriptionchange', function () {
const fetchOptions = {
method: 'post'
,
};
fetch('http://localhost:5333/usru.ashx', fetchOptions)
.then(function (response) {
if (response.status >= 400 && response.status < 500) {
console.log('Failed web push response: ', response, response.status);
throw new Error('Failed to update users.');
}
})
.catch((err) => {
this.showErrorMessage('Ooops Unable to Send a user', err);
});
});
I have subscribed the users successfully using the following code:
registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('http://localhost:5333/service-worker.js')
.catch((err) => {
this.showErrorMessage('Unable to Register SW', 'Sorry this demo requires a service worker to work and it ' + 'failed to install - sorry :(');
console.error(err);
});
} else {
this.showErrorMessage('Service Worker Not Supported', 'Sorry this demo requires service worker support in your browser. ' +
'Please try this demo in Chrome or Firefox Nightly.');
}
}
and
class PushClient {
constructor(subscriptionUpdate, appkeys) {
this._subscriptionUpdate = subscriptionUpdate;
this._publicApplicationKey = appkeys;
if (!('serviceWorker' in navigator)) {
return;
}
if (!('PushManager' in window)) {
return;
}
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
return;
}
navigator.serviceWorker.ready.then(() => {
this.setUpPushPermission();
});
}
setUpPushPermission() {
return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.getSubscription();
})
.then((subscription) => {
if (!subscription) {
return;
}
this._subscriptionUpdate(subscription);
})
.catch((err) => {
console.log('setUpPushPermission() ', err);
});
}
subscribeDevice() {
return new Promise((resolve, reject) => {
if (Notification.permission === 'denied') {
sc(3);
return reject(new Error('Push messages are blocked.'));
}
if (Notification.permission === 'granted') {
sc(3);
return resolve();
}
if (Notification.permission === 'default') {
Notification.requestPermission((result) => {
if (result === 'denied') {
sc(0);
} else if (result === 'granted') {
sc(1);
} else {
sc(2);
}
if (result !== 'granted') {
reject(new Error('Bad permission result'));
}
resolve();
});
}
})
.then(() => {
return navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true
, applicationServerKey: this._publicApplicationKey.publicKey
,
});
})
.then((subscription) => {
this._subscriptionUpdate(subscription);
if (subscription) {
this.sendPushMessage(subscription);
}
})
.catch((subscriptionErr) => { });
})
.catch(() => { });
}
toBase64(arrayBuffer, start, end) {
start = start || 0;
end = end || arrayBuffer.byteLength;
const partialBuffer = new Uint8Array(arrayBuffer.slice(start, end));
return btoa(String.fromCharCode.apply(null, partialBuffer));
}
unsubscribeDevice() {
navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
return serviceWorkerRegistration.pushManager.getSubscription();
})
.then((pushSubscription) => {
if (!pushSubscription) {
this._subscriptionUpdate(null);
return;
}
return pushSubscription.unsubscribe()
.then(function (successful) {
if (!successful) {
console.error('We were unable to unregister from push');
}
});
})
.then(() => {
this._subscriptionUpdate(null);
})
.catch((err) => {
console.error('Error thrown while revoking push notifications. ' + 'Most likely because push was never registered', err);
});
}
sendPushMessage(subscription) {
let payloadPromise = Promise.resolve(null);
payloadPromise = JSON.parse(JSON.stringify(subscription));
const vapidPromise = EncryptionHelperFactory.createVapidAuthHeader(this._publicApplicationKey, subscription.endpoint, 'http://localhost:5333/');
return Promise.all([payloadPromise, vapidPromise, ])
.then((results) => {
const payload = results[0];
const vapidHeaders = results[1];
let infoFunction = this.getWebPushInfo;
infoFunction = () => {
return this.getWebPushInfo(subscription, payload, vapidHeaders);
};
const requestInfo = infoFunction();
this.sendRequestToProxyServer(requestInfo);
});
}
getWebPushInfo(subscription, payload, vapidHeaders) {
let body = null;
const headers = {};
headers.TTL = 60;
if (payload) {
headers.Encryption = `auth=${payload.keys.auth}`;
headers['Crypto-Key'] = `p256dh=${payload.keys.p256dh}`;
headers['Content-Encoding'] = 'aesgcm';
} else {
headers['Content-Length'] = 0;
}
if (vapidHeaders) {
headers.Authorization = `WebPush ${vapidHeaders.authorization}`;
if (headers['Crypto-Key']) {
headers['Crypto-Key'] = `${headers['Crypto-Key']}; ` + `p256ecdsa=${vapidHeaders.p256ecdsa}`;
} else {
headers['Crypto-Key'] = `p256ecdsa=${vapidHeaders.p256ecdsa}`;
}
}
const response = {
headers: headers
, endpoint: subscription.endpoint
,
};
if (body) {
response.body = body;
}
return response;
}
sendRequestToProxyServer(requestInfo) {
const fetchOptions = {
method: 'post'
,
};
if (requestInfo.body && requestInfo.body instanceof ArrayBuffer) {
requestInfo.body = this.toBase64(requestInfo.body);
fetchOptions.body = requestInfo;
}
fetchOptions.body = JSON.stringify(requestInfo);
fetch('http://localhost:5333/usrh.ashx', fetchOptions)
.then(function (response) {
if (response.status >= 400 && response.status < 500) {
console.log('Failed web push response: ', response, response.status);
throw new Error('Failed to send push message via web push protocol');
}
})
.catch((err) => {
this.showErrorMessage('Ooops Unable to Send a Push', err);
});
}
}
All these codes are in javascript. I can successfully recieve user subscription infromarion on my server like:
Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwcxxxxx
Crypto-Key: p256dh=BBp90dwDWxxxxc1TfdBjFPqxxxxxwjO9fCip-K_Eebmg=; p256ecdsa=BDd3_hVL9fZi9Yboxxxxxxo
endpoint: https://fcm.googleapis.com/fcm/send/cxxxxxxxxxxxxxxJRorOMHKLQ3gtT7
Encryption: auth=9PzQZ1mut99qxxxxxxxxxxyw==
Content-Encoding: aesgcm
Also I can successfully send a push to this user using the code bellow in C#:
public static async Task<bool> SendNotificationByte(string endpoint, string[] Keys, byte[] userSecret, byte[] data = null,
int ttl = 0, ushort padding = 0, bool randomisePadding = false, string auth="")
{
#region send
HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);
Request.Headers.TryAddWithoutValidation("Authorization", auth);
Request.Headers.Add("TTL", ttl.ToString());
if (data != null && Keys[1] != null && userSecret != null)
{
EncryptionResult Package = EncryptMessage(Decode(Keys[1]), userSecret, data, padding, randomisePadding);
Request.Content = new ByteArrayContent(Package.Payload);
Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Request.Content.Headers.ContentLength = Package.Payload.Length;
Request.Content.Headers.ContentEncoding.Add("aesgcm");
Request.Headers.Add("Crypto-Key", "dh=" + Encode(Package.PublicKey)+" ;"+Keys[2]+"="+Keys[3]);
Request.Headers.Add("Encryption", "salt=" + Encode(Package.Salt));
}
using (HttpClient HC = new HttpClient())
{
HttpResponseMessage res = await HC.SendAsync(Request).ConfigureAwait(false);
if (res.StatusCode == HttpStatusCode.Created)
return true;
else return false;
}
#endregion
}
The problem is that after a period of time (about 20 hours or even less), when I want to send a push to this user I got the following errors:
firefox subscription:
{StatusCode: 410, ReasonPhrase: 'Gone', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Access-Control-Allow-Headers: content-encoding,encryption,crypto-key,ttl,encryption-key,content-type,authorization
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: location,www-authenticate
Connection: keep-alive
Cache-Control: max-age=86400
Date: Tue, 21 Feb 2017 08:19:03 GMT
Server: nginx
Content-Length: 179
Content-Type: application/json
}}
chrome subscription:
{StatusCode: 400, ReasonPhrase: 'UnauthorizedRegistration', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Alt-Svc: quic=":443"; ma=2592000; v="35,34"
Vary: Accept-Encoding
Transfer-Encoding: chunked
Accept-Ranges: none
Cache-Control: max-age=0, private
Date: Tue, 21 Feb 2017 08:18:35 GMT
Server: GSE
Content-Type: text/html; charset=UTF-8
Expires: Tue, 21 Feb 2017 08:18:35 GMT
}}
I think I missed something, that makes the subscription expires, or have to make the users to resubscribe when their subscription information is changed or expired, but I do not know how?!!
The Problem is solved by sending a push echo notification to the subscribed users to resubscribe them. I have wrote a job in which I send a push echo periodically and resubscribe the users and update their information.
To do so I send an special message called "push echo" using the code bellow:
self.addEventListener('push', function (event) {
lastEventName = 'push';
var msg = {};
if (event.data) {
msg = event.data.json();
if (!!msg.isEcho) {
self.registration.pushManager.getSubscription()
.then(function (subscription) {
if (!subscription) {
} else {
subscription.unsubscribe().then(function () {
self.registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array('xxxxxxxxxxxxxxxx') })
.then(function (subscription) {
resubscription(subscription);
});
});
}
});
return;
}
}
if (!!msg.isEcho)
return;
let notificationTitle = msg.title;
const notificationOptions = {
body: msg.body,
dir: 'rtl',
icon: msg.icon,
data: {
url: msg.url,
id: msg.id,
key: msg.key
},
};
event.waitUntil(
Promise.all([
self.registration.showNotification(
notificationTitle, notificationOptions),
])
);
const fetchOptions =
{ method: 'post', mode: 'no-cors' };
fetch('http://example.com', fetchOptions).
then(function (response) {
if (response.status >= 400 && response.status < 500) {
throw new Error('Failed to send push message via web push protocol');
}
lastEventName = 'view';
}).catch((err) => {
this.showErrorMessage('Ooops Unable to Send a Click', err);
});
});
In the resubscription method you can unsubscribe and then subscribe the user and update server data.
I think the issue is about how you send your applicationServerKey. I just have done an example of what you want to do and I had to send that key encoded with this function:
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
so you have to create your subscription object in this way :
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(this._publicApplicationKey.publicKey),
})
Mainly what I did was follow this tutorial.
I have that working example in this github repo. The README file is in Spanish, but I think it can help you.
I think the problem can be solved by resubscribing the users.
Some clues:
The sw needs to be registered and activated, at the time the push event arrives. This mean that you may not clean session, use private browsing mode, clean cache of computer in between.
The push event must come from the same origin.

Categories

Resources