nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
66.89k stars 7.56k forks source link

Any way to make the gateway listen the same port as the application? #126

Closed alejandroSuch closed 6 years ago

alejandroSuch commented 7 years ago

Hi there!

Is there any way to make an equivalent to this classic configuration in nest?

var app = express()
  , server = require('http').createServer(app)
  , io = io.listen(server);

server.listen(3000);

My application has only one gateway and I would like it to use the same port as the main express application.

Thanks a lot! Alex

cojack commented 7 years ago

@alejandroSuch https://docs.nestjs.com/quick-start/gateways.html

By default - server runs on port 80 and with default namespace. We can easily change those settings:

How about do not add port in config?

alejandroSuch commented 7 years ago

Yes but what I want to do is to listen the same port as the express application (i.e. port 3000). To do this, you have to attach the socket.io server to a HttpServer instance instead of doing it to a port.

bradleyayers commented 7 years ago

@alejandroSuch did you end up wiring together a solution for this?

alejandroSuch commented 7 years ago

I need to know if there's any way to get the http server instance from the websocket initializer. If there's a way, it's quite simple

kamilmysliwiec commented 7 years ago

Hi @alejandroSuch, Not possible now, definitely needed, marked as a todo. Thanks!

bradleyayers commented 7 years ago

For now I just wrote my own adapter: (I also use ws rather than socket.io)

class WebSocketAdapter
  implements NestWebSocketAdapter {
  private readonly logger = this.loggingService.create(WebSocketAdapter.name);

  constructor(
    private readonly server: http.Server,
    private readonly path: string,
    private readonly authService: AuthService,
    private readonly loggingService: LoggingService
  ) {}

  public create() {
    return new WebSocket.Server({
      server: this.server,
      path: this.path,
      verifyClient: ({ req }: { req: http.IncomingMessage }) => {
        const jwt = this.getJwtFromRequest(req);
        return (
          jwt && !(this.authService.verifyAndDecode(jwt) instanceof Error)
        );
      }
    });
  }

  public bindClientConnect(
    server: WebSocket.Server,
    callback: (client: Client) => void
  ) {
    server.on("connection", (socket, req) => {
      // Coercion to nonnull and Claims is possible because we provide
      // `verifyClient` to WebSocket.Server.
      const jwt = this.getJwtFromRequest(req);
      const claims = this.authService.skipVerifyAndDecode(jwt!) as Claims;
      const client = new Client(socket, claims);
      callback(client);
    });
  }

  // Explicit `this` preservation due to https://github.com/kamilmysliwiec/nest/issues/150
  public bindClientDisconnect = (
    client: Client,
    callback: (client: Client) => void
  ) => {
    client.ws.on("close", () => {
      callback(client);
    });
  };

  public bindMessageHandlers(
    client: Client,
    handlers: MessageMappingProperties[]
  ) {
    client.ws.on("message", async data => {
      if (typeof data !== "string") {
        this.logger.warn(
          `Received "${typeof data}" message, expected: "string"`
        );
        return;
      }

      let json: { [k: string]: any };

      // Parse
      try {
        json = JSON.parse(data);
      } catch (e) {
        this.logger.error(
          `Error while JSON decoding message`,
          e instanceof Error ? e.stack : undefined
        );
        return;
      }

      // Verify shape
      const type = json["type"];
      if (typeof type !== "string") {
        this.logger.error(
          `Invalid "type" field ("${typeof type}"), expected: "string"`
        );
        return;
      }

      // Find a message handler
      const handler = handlers.find(h => h.message === type);
      if (!handler) {
        this.logger.warn(`Received unknown message "${type}"`);
        return;
      }

      // Execute handler
      try {
        await handler.callback(json);
      } catch (e) {
        this.logger.error(
          `Error occurred during "${type}" handler`,
          e instanceof Error ? (e.stack ? e.stack : e.message) : undefined
        );
        return;
      }
    });
  }

  private getJwtFromRequest(req: http.IncomingMessage): string | null {
    if (req.url) {
      const { query } = url.parse(req.url, true);
      if (query && typeof query.jwt === "string") {
        return query.jwt;
      }
    }
    return null;
  }
}

/**
 * A wrapper to associate claims (from the URL) with a WebSocket. This allows us
 * to know who is connected to each socket.
 */
class Client {
  constructor(public readonly ws: WebSocket, public readonly claims: Claims) {}

  public send(message) {
    this.ws.send(JSON.stringify(message));
  }
}

And then use my own HTTP server:

const express = Express();
const server = http.createServer(express);

express.use(morgan(config.DEBUG ? "dev" : "combined"));
express.use(bodyParser.json());
express.use(cors());
express.use(compression());

const nest = NestFactory.create(ApplicationModule, express);

const environmentService = new EnvironmentService();

nest.useGlobalFilters(new DbErrorFilter(), new JsonValidationErrorFilter());
nest.useWebSocketAdapter(
  new RealtimeGateway.WebSocketAdapter(
    server,
    "/realtime",
    new AuthService(environmentService),
    new LoggingService(environmentService)
  )
);
nest.init();

server.listen(config.PORT, () => {
  new Logger("http").log(`Listening on port ${config.PORT}`);
});
alejandroSuch commented 7 years ago

Cool! Wouln't be difficult to adapt the same philosophy to any implementation (socket.io, sockjs).

xujif commented 7 years ago

there is an easily way to extend IoAdapter

import { IoAdapter } from '@nestjs/websockets';
import * as io from 'socket.io'
import * as http from 'http'
export class ExtendedSocketIoAdapter extends IoAdapter {
    protected ioServer: SocketIO.Server
    constructor(protected server: http.Server) {
        super()
        this.ioServer = io(server)
    }

    create (port: number) {
        console.log('websocket gateway port argument is ignored by ExtendedSocketIoAdapter, use the same port of http instead')
        return this.ioServer
    }
}

and server.js bootstrap

    const expressApp = require('express')();
    const server = require('http').createServer(expressApp);
    const app = await NestFactory.create(ApplicationModule, expressApp);
    app.useWebSocketAdapter(new ExtendedSocketIoAdapter(server))
    await app.init()
    server.listen(8000);

and I suggest "WebSocketGateway" default to create an io instance with the same port.

cojack commented 6 years ago

@xujif does your solution doesn't throws you an error:

GET http://localhost:3000/socket.io/?EIO=3&transport=polling&t=Lyv3Nn7 404 (Not Found) ?

joshwiens commented 6 years ago

I'm running with https.Server wrapping io server in a very similar manner on 4433 for everything & it works just fine.

cojack commented 6 years ago

@d3viant0ne can you show your code?

kamilmysliwiec commented 6 years ago

Hi @alejandroSuch, It's available from v4.2.0. (just left @WebSocketGateway() empty)

lock[bot] commented 4 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.