sveltekit/vite problem with self signed certificate on localhost - socket.io

In my dev environment I'm using the basicSsl-Plugin for generating a self-signed-certificate. The website works fine under https until the fetch function is trying to delete a user.
my vite.config.js:
plugins: [
basicSsl(),
sveltekit(),
{
name: 'sveltekit-socket-io',
configureServer(server) {
const io = new Server(server.httpServer);
io.on('connection', (socket) => {
const agent = new https.Agent({
rejectUnauthorized: false
});
socket.on('disconnect', () => {
await fetch('https://localhost:5173/api/users', {
method: 'DELETE',
body: JSON.stringify({ id: socket.uid }),
agent: agent
});
});
});
i get the error Error: self-signed certificate and code DEPTH_ZERO_SELF_SIGNED_CERT.
when instead of using the basicSsl-Plugin I try using using mkcert-created self signed certificates I cant even access the website anymore with https because I get the following error in the browser: ERR_SSL_VERSION_OR_CIPHER_MISMATCH.

I added process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; and now it seems to work. I guess it's fine for development purposes, don't use it on production.

Related

Cypress: browser authentication not working inside a custom command

I have a website which needs browser authentication on all page visits. If I use the below code on all page visits it works fine:
describe('WW2-3461 validate patches for contrib modules and core', () => {
it('should visit my base URL', () => {
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
})
})
I am passing the base_url as an environment through CLI:
npx cypress run --spec file.spec.js --env base_url=$base_url
I don't want to use browserauthentication on all page visits, so I created a custom command like below (in integration/project/utility.js file):
Cypress.Commands.add('addBrowserAuthentication', (url) => {
cy.visit(url, {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
})
However if I call this command inside a spec file, like below:
import './utility.spec'
describe('WW2-3461 validate patches for contrib modules and core', () => {
beforeEach(() => {
cy.addBrowserAuthentication(Cypress.env('cypress_host'))
})
it('should visit my base URL', () => {
cy.visit(Cypress.env('cypress_host'), {
})
})
the browser authentication does not seem to work (tried to call it inside before() hook or directly inside the test scenario) as it returns
> 401: Unauthorized
This was considered a failure because the status code was not `2xx`.
This http request was redirected '1' time to:
- 302: https://mysite.local
If you do not want status codes to cause failures pass the option: `failOnStatusCode: false`
I can't seem to work around this issue. Tried using cy.request() but getting the same issue. Has someone faced this issue?
You don't need to pass env variable as a parameter, you can directly access it inside the custom command.
Cypress.Commands.add('addBrowserAuthentication', () => {
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password,
},
failOnStatusCode: false,
})
})
And in your test directly use:
cy.addBrowserAuthentication()
Also I am not sure if its required to visit the website two times. So you can remove cy.visit(Cypress.env('cypress_host') if you are already using cy.addBrowserAuthentication().
Since this works
cy.visit(Cypress.env('base_url'), {
// Add basic auth headers
auth: {
username: Cypress.env('browserAuthentication').username,
password: Cypress.env('browserAuthentication').password
},
failOnStatusCode: false
})
but this doesn't
cy.addBrowserAuthentication(Cypress.env('cypress_host'))
it looks like Cypress.env('cypress_host') is not the same as Cypress.env('base_url').
Everything else looks good in your command, so I'd try with
cy.addBrowserAuthentication(Cypress.env('base_url'))
When you cy.visit(Cypress.env('cypress_host') without headers inside the test, the auth headers from addBrowserAuthentication are lost. I suggest removing that cy.visit().

socket.io WebSocket connection failed

I am using socket.io to connect to a different domain, and it can successfully connect using polling, however when attempting to connect using websockets gets the error "WebSocket connection to 'wss://XXXXX' failed".
After observing the network activity the server seems to indicate that it is capable of upgrading to a websocket connection (although I won't claim to be an expert in understanding these requests), but isn't able to do so:
I'm just trying to produce a minimal viable product right now so here is my node.js server code:
let http = require('http');
let https = require('https');
let fs = require('fs');
let express = require('express');
const privateKey = fs.readFileSync('XXXXXXXXX', 'utf8');
const certificate = fs.readFileSync('XXXXXXXXX', 'utf8');
const ca = fs.readFileSync('XXXXXXXXX', 'utf8');
const options = {
key: privateKey,
cert: certificate,
ca: ca
};
let app = express();
let httpsServer = https.createServer(options,app);
let io = require('socket.io')(httpsServer, {
cors: {
origin: true
}
});
httpsServer.listen(443);
console.log('starting');
io.sockets.on('connection', (socket) => {
console.log("something is happening right now")
socket.on("salutations", data => {
console.log(`you are now connected via ${socket.conn.transport.name}`);
socket.emit("greetings", "I am the socket confirming that we are now connected");
});
});
Client-side JavaScript:
const socket = io("https://XXXXXXX");
console.log(socket);
socket.on("connect", () => {
console.log("now connected");
socket.on("message", data => {
console.log(data);
});
socket.on("greetings", (elem) => {
console.log(elem);
});
});
let h1 = document.querySelector('h1');
h1.addEventListener('click',()=>{
console.log("I am now doing something");
socket.emit("salutations", "Hello!");
})
The only suggestion in the official documentation for this issue isn't relevant because I'm not using a proxy, and other suggested fixes result in no connection at all (presumably because they prevent it from falling back to polling)
EDIT: also if it helps narrow down the problem, when querying my server using https://www.piesocket.com/websocket-tester it results in "Connection failed, see your browser's developer console for reason and error code"

Apollo Express Server on Heroku and Refresh Token Cookie on Mobile Browser

Upon visiting/refresh, the app checks for a refresh token in the cookie. If there is a valid one, an access token will be given by the Apollo Express Server. This works fine on my desktop but when using Chrome or Safari on the iPhone, the user gets sent to the login page on every refresh.
React App with Apollo Client
useEffect(() => {
fetchUser();
}, []);
const fetchUser = async () => {
const res = await fetch('https://website.com/token', {
method: 'POST',
credentials: 'include',
});
const { accessToken } = await res.json();
if (accessToken === '') {
setIsLoggedIn(false);
}
setAccessToken(accessToken);
setLoading(false);
};
Apollo Client also checks if whether the access token is valid
const authLink = setContext((_, { headers }) => {
const token = getAccessToken();
if (token) {
const { exp } = jwtDecode(token);
if (Date.now() <= exp * 1000) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
}
}
fetch('https://website.com/token', {
method: 'POST',
credentials: 'include',
}).then(async (res) => {
const { accessToken } = await res.json();
setAccessToken(accessToken);
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${accessToken}` : '',
},
};
});
});
const client = new ApolloClient({
link: from([authLink.concat(httpLink)]),
cache: new InMemoryCache(),
connectToDevTools: true,
});
This handles the token link on the Express server
app.use('/token', cookieParser());
app.post('/token', async (req, res) => {
const token = req.cookies.rt;
if (!token) {
return res.send({ ok: false, accessToken: '' });
}
const user = await getUser(token);
if (!user) {
return res.send({ ok: false, accessToken: '' });
}
sendRefreshToken(res, createRefreshToken(user));
return res.send({ ok: true, accessToken: createAccessToken(user) });
});
And setting of the cookie
export const sendRefreshToken = (res, token) => {
res.cookie('rt', token, {
httpOnly: true,
path: '/token',
sameSite: 'none',
secure: true,
});
};
Same site is 'none' as the front end is on Netlify.
After a day of fiddling and researching, I have found the issue, and one solution when using a custom domain.
The issue is that iOS treats sameSite 'none' as sameSite 'strict'. I thought iOS Chrome would be different than Safari but it appears not.
If you use your front-end, hosted on Netlify, you will naturally have a different domain than your Heroku app back-end. Since I am using a custom domain, and Netlify provides free SSL, half of the work is done.
The only way to set a httpOnly cookie is to set the cookie to secure. The next step would be to set sameSite to 'none' but as mentioned above, this does not work with iOS.
Setting the domain property of the cookie will also not work because the domain property concerns the scope of the cookie and not the cookie origin. If the cookie came from a different domain (Heroku backend), then the frontend (on Netlify) will not be able to use it.
By default, on Heroku, the free dyno will give you a domain like 'your-app.herokuapp.com', which is great because it also includes free SSL. However, for the cookie to work, I added my custom domain that I use with Netlify. To be clear, Netlify already uses my apex custom domain, so I am adding a subdomain to Heroku (api.domain.com). Cookies do work for across the same domain and subdomains with sameSite 'strict'.
The final issue with this is that the custom domain with Heroku will not get SSL automatically, which is why I think it is worth it to upgrade to a $7/month hobby dyno to avoid managing the SSL manually. This I think is the only solution when using a custom domain.
On the other hand, for those who have the same issue and would like a free solution, you can forgo using a custom domain and host your static front-end with the back-end on Heroku.
Hopefully this will save some time for anyone deploying the back-end and front-end separately.

How to set up a socket connection on a strapi server

I am trying to integrate socket.io with strapi. But unfortunately I have been unable to do so without any proper tutorial or documentation covering this aspect.
I followed along with the only resource I found online which is:
https://medium.com/strapi/strapi-socket-io-a9c856e915a6
But I think the article is outdated. I can't seem to run the code mentioned in it without running into tonnes of errors.
Below is my attempt to implement it and I have been trying to connect it through a chrome websocket plugin smart websocket client But I am not getting any response when I try to run the server.
I'm totally in the dark. Any help will be appreciated
module.exports = ()=> {
// import socket io
var io = require('socket.io')(strapi.server)
console.log(strapi.server) //undefined
// listen for user connection
io.on('connect', socket => {
socket.send('Hello!');
console.log("idit")
// or with emit() and custom event names
socket.emit('greetings', 'Hey!', { 'ms': 'jane' }, Buffer.from([4, 3, 3, 1]));
// handle the event sent with socket.send()
socket.on('message', (data) => {
console.log(data);
});
// handle the event sent with socket.emit()
socket.on('salutations', (elem1, elem2, elem3) => {
console.log(elem1, elem2, elem3);
});
});
};
So I found the solution. Yay. I'll put it here just in case anybody needs it.
boostrap.js
module.exports = async () => {
process.nextTick(() =>{
var io = require('socket.io')(strapi.server);
io.on('connection', async function(socket) {
console.log(`a user connected`)
// send message on user connection
socket.emit('hello', JSON.stringify({message: await strapi.services.profile.update({"posted_by"})}));
// listen for user diconnect
socket.on('disconnect', () =>{
console.log('a user disconnected')
});
});
strapi.io = io; // register socket io inside strapi main object to use it globally anywhere
})
};
Found this at: https://github.com/strapi/strapi/issues/5869#issuecomment-619508153_
Apparently, socket.server is not available when the server starts. So you have to make use of process.nextTick that waits for the socket.server to initialize.
I'll also add a few questions that I faced when setting this up.
How do i connect from an external client like nuxt,vue or react?
You just have to connect through "http://localhost:1337" that is my usual address for strapi.
I am using nuxt as my client side and this is how set up my socketio on the client side
I first installed nuxt-socket-io through npm
Edited the nuxt.config file as per it's documention
modules:[
...
'nuxt-socket-io',
...
],
io: {
// module options
sockets: [
{
name: 'main',
url: 'http://localhost:1337',
},
],
},
And then i finally added a listener in one of my pages.
created() {
this.socket = this.$nuxtSocket({})
this.socket.on('hello', (msg, cb) => {
console.log('SOCKET HI')
console.log(msg)
})
},
And it works.
A clean way to integrate third-party services into Strapi is to use hooks. They are loaded once during the server boot. In this case, we will create a local hook.
The following example has worked with strapi#3.6.
Create a hook for socket.io at ./hooks/socket.io/index.js
module.exports = strapi => {
return {
async initialize() {
const ioServer = require('socket.io')(strapi.server, {
cors: {
origin: process.env['FRONT_APP_URL'],
methods: ['GET', 'POST'],
/* ...other cors options */
}
})
ioServer.on('connection', function(socket) {
socket.emit('hello', `Welcome ${socket.id}`)
})
/* HANDLE CLIENT SOCKET LOGIC HERE */
// store the server.io instance to global var to use elsewhere
strapi.services.ioServer = ioServer
},
}
}
Enable the new hook in order for Strapi to load it - ./config/hook.js
module.exports = {
settings: {
'socket.io': {
enabled: true,
},
},
};
That's done. You can access the websocket server inside ./config/functions/bootstrap.js or models' lifecycle hooks.
// ./api/employee/models/employee.js
module.exports = {
lifecycles: {
async afterUpdate(result, params, data) {
strapi.services.ioServer.emit('update:employee', result)
},
},
};
For those who are looking the answer using Strapi version 4
var io = require("socket.io")(strapi.server.httpServer)

Release job on Heroku randomly stops sending Files over FTP

I have an app release process that has been working fine for a week or two and has now randomly stopped working.
My npm app is built with Heroku and a release job then runs that FTPs the static files to another host. I use the npm ftp library to do this. This has randomly stopped working with a timeout error:
Error: Timeout while connecting to server
at Timeout._onTimeout (/app/node_modules/ftp/lib/connection.js:304:24)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10)
the release script is as follows:
const DIST_PATH = `dist/cineworld-planner`;
const filesList = readdirSync(join(process.cwd(), DIST_PATH));
const client = new Client();
function pushFiles() {
const putFile = bindNodeCallback(client.put.bind(client));
from(filesList).pipe(
mergeMap(fileName => {
const localPath = join(process.cwd(), DIST_PATH, fileName);
console.log(`Putting path ${localPath} to remote ${fileName}`);
return putFile(localPath, fileName);
}, 1)
).subscribe({
complete: () => client.end()
});
}
client.on('ready', () => {
console.log(`READY`);
pushFiles();
});
client.on('error', (error: any) => {
const code = error.code || 'NO_CODE';
console.log(`ERROR: ${code}`);
console.log(error);
process.exit(1);
});
client.connect({
user: process.env.FTP_USER,
host: process.env.FTP_HOST,
password: process.env.FTP_PASSWORD
});
I have asked my host if there are any issues but all they have said is that the IP address that my script reported it was running on was not blocked.
I have tested the script from my home PC and it also works fine from there.
This will be a major pain if this has stopped working. I really don't know what else to try.
Any help very gratefully received.
It turns out that for me the fix was as simple as increasing the timeout:
const connectOptions: Options = {
user: process.env.FTP_USER,
host: process.env.FTP_HOST,
password: process.env.FTP_PASSWORD,
connTimeout: 60000,
pasvTimeout: 60000,
};
client.connect(connectOptions);

Resources