Skip to content

Commit

Permalink
feat: add support for in-band-registration (xep-0077)
Browse files Browse the repository at this point in the history
  • Loading branch information
trampi committed Nov 8, 2018
1 parent 5274c84 commit 1205d2f
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 6 deletions.
3 changes: 0 additions & 3 deletions projects/pazznetwork/ngx-chat/src/lib/core/stanza.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export interface Stanza extends Element {
};
children: Stanza[];
name: string;

getChild(child: string): Stanza;

}

export interface IqResponseStanza extends Stanza {
Expand Down
2 changes: 2 additions & 0 deletions projects/pazznetwork/ngx-chat/src/lib/ngx-chat.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from './services/adapters/xmpp/plugins';
import { MessagePlugin } from './services/adapters/xmpp/plugins/message.plugin';
import { PingPlugin } from './services/adapters/xmpp/plugins/ping.plugin';
import { RegistrationPlugin } from './services/adapters/xmpp/plugins/registration.plugin';
import { XmppChatAdapter } from './services/adapters/xmpp/xmpp-chat-adapter.service';
import { XmppChatConnectionService } from './services/adapters/xmpp/xmpp-chat-connection.service';
import { ChatListStateService } from './services/chat-list-state.service';
Expand Down Expand Up @@ -102,6 +103,7 @@ export class NgxChatModule {
new ServiceDiscoveryPlugin(xmppChatAdapter),
new PushPlugin(xmppChatAdapter),
new PingPlugin(xmppChatAdapter),
new RegistrationPlugin(logService),
]);

return xmppChatAdapter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export * from './message-uuid.plugin';
export * from './multi-user-chat.plugin';
export * from './publish-subscribe.plugin';
export * from './push.plugin';
export * from './registration.plugin';
export * from './roster.plugin';
export * from './service-discovery.plugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { client } from '@xmpp/client';
import { Client } from '@xmpp/client-core';
import getDomain from '@xmpp/client/lib/getDomain';
import { timeout } from '@xmpp/events';
import { x as xml } from '@xmpp/xml';
import { Subject } from 'rxjs';
import { first, takeUntil } from 'rxjs/operators';
import { LogService } from '../../../log.service';
import { AbstractXmppPlugin } from './abstract-xmpp-plugin';

/**
* xep-0077
*/
export class RegistrationPlugin extends AbstractXmppPlugin {

private readonly xmppRegistrationComplete$ = new Subject();
private readonly xmppRegistrationFinally$ = new Subject();
private readonly xmppLoggedIn$ = new Subject();
private readonly registrationTimeout = 5000;
private client: Client;

constructor(private logService: LogService) {
super();
}

/**
* Promise resolves if user account is registered successfully,
* rejects if an error happens while registering, e.g. the username is already taken.
*/
public async register(username: string,
password: string,
service: string,
domain?: string): Promise<void> {
try {
await timeout((async () => {
domain = domain || getDomain(service);

this.logService.debug('registration plugin', 'connecting...');
await this.connect(username, password, service, domain);

this.logService.debug('registration plugin', 'connection established, starting registration');
await this.announceRegistration(domain);

this.logService.debug('registration plugin', 'server acknowledged registration request, sending credentials');
await this.writeRegister(username, password);
this.xmppRegistrationComplete$.next();

this.logService.debug('registration plugin', 'registration successful');
await this.xmppLoggedIn$.pipe(first(), takeUntil(this.xmppRegistrationFinally$)).toPromise();
this.logService.debug('registration plugin', 'logged in');

this.logService.debug('registration plugin', 'saving encrypted credentials');
})(), this.registrationTimeout);
} catch (e) {
this.logService.error('error registering', e);
throw e;
} finally {
this.logService.debug('registration plugin', 'cleaning up');
this.xmppRegistrationFinally$.next();
await this.client.stop();
this.client.removeAllListeners();
}
}

private connect(username: string, password: string, service: string, domain?: string) {
return new Promise(resolveConnectionEstablished => {
this.client = client({
service,
domain,
credentials: (proceedWithLogin: any) => {
return new Promise(credentialsResolve => {
resolveConnectionEstablished();
// wait until registration is successful and pass the credentials
this.xmppRegistrationComplete$.pipe(
first(),
takeUntil(this.xmppRegistrationFinally$)
).subscribe(() => {
this.logService.debug('registration plugin', 'proceeding');
proceedWithLogin({username, password}).then(() => credentialsResolve());
});
});
}
});

this.client.timeout = this.registrationTimeout;

this.client.on('online', () => {
this.logService.debug('registration plugin', 'online event');
this.xmppLoggedIn$.next();
});

this.client.on('error', (err: any) => {
this.logService.error('registration plugin', err);
});

this.client.on('offline', () => {
this.logService.debug('registration plugin', 'offline event');
});

this.client.reconnect.stop();
this.client.start();
});
}

private writeRegister(username: string, password: string) {
return new Promise(async (resolve, reject) => {
try {
const result = await this.client.iqCaller.request(
xml('iq', {type: 'set'},
xml('query', {xmlns: 'jabber:iq:register'},
xml('username', {}, username),
xml('password', {}, password)
)
)
);
if (result.attrs.type === 'result') {
resolve();
}
} catch (e) {
reject(e);
}
});
}

private async announceRegistration(domain: string) {
await this.client.iqCaller.request(
xml('iq', {type: 'get', to: domain},
xml('query', {xmlns: 'jabber:iq:register'})
)
);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
declare module '@xmpp/client-core' {

import { Element } from 'ltx';

export class Client {

public timeout: number;

public startOptions: { uri: string, domain: string };
public reconnect: Reconnect;

public iqCaller: IqCaller;

public startOptions: StartOptions;

public on(eventName: string, callback: any): void;

Expand All @@ -20,11 +26,25 @@ declare module '@xmpp/client-core' {

public plugin(plugin: any): void;

public removeAllListeners(): void;

}

export interface StartOptions {
uri: string;
domain: string;
}

export interface Reconnect {

stop(): void;

}

export interface IqCaller {

request(stanza: Element): Promise<Element>;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module '@xmpp/client' {

import { Client } from '@xmpp/client-core';

export function client(...args): Client;

}
5 changes: 5 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@

<button (click)="onLogin()">Log in</button>
<button (click)="onLogout()">Log out</button>
<button (click)="onRegister()">Register</button>

<div *ngIf="registrationMessage">
<p>{{registrationMessage}}</p>
</div>

<hr />

Expand Down
31 changes: 29 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { Component, Inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import { ChatService, ChatServiceToken, Contact, ContactFactoryService, MultiUserChatPlugin } from './ngx-chat-imports';
import {
ChatService,
ChatServiceToken,
Contact,
ContactFactoryService,
LogLevel,
LogService,
MultiUserChatPlugin,
RegistrationPlugin,
} from './ngx-chat-imports';

@Component({
selector: 'app-root',
Expand All @@ -16,10 +25,13 @@ export class AppComponent {
public otherJid: any;
public contacts: Observable<Contact[]> = this.chatService.contactsSubscribed$;
public multiUserChatPlugin: MultiUserChatPlugin;
public registrationMessage: string;

constructor(@Inject(ChatServiceToken) public chatService: ChatService,
private contactFactory: ContactFactoryService) {
private contactFactory: ContactFactoryService,
private logService: LogService) {
const contactData: any = JSON.parse(localStorage.getItem('data')) || {};
this.logService.logLevel = LogLevel.Debug;
this.domain = contactData.domain;
this.service = contactData.service;
this.password = contactData.password;
Expand All @@ -46,6 +58,21 @@ export class AppComponent {
this.chatService.logOut();
}

async onRegister() {
this.registrationMessage = 'registering ...';
try {
await this.chatService.getPlugin(RegistrationPlugin).register(
this.username,
this.password,
this.service,
this.domain
);
this.registrationMessage = 'registration successful';
} catch (e) {
this.registrationMessage = 'registration failed: ' + e.toString();
throw e;
}
}

onAddContact() {
this.chatService.addContact(this.otherJid);
Expand Down

0 comments on commit 1205d2f

Please sign in to comment.