Skip to content

Commit 1cdcda8

Browse files
authored
Merge pull request #2862 from RestComm/restcomm-1580
Restcomm 1580 - Support Custom Header for Dial verb
2 parents be0a511 + 8201533 commit 1cdcda8

19 files changed

Lines changed: 1405 additions & 141 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* TeleStax, Open Source Cloud Communications
3+
* Copyright 2011-2018, Telestax Inc and individual contributors
4+
* by the @authors tag.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* under the terms of the GNU Affero General Public License as
8+
* published by the Free Software Foundation; either version 3 of
9+
* the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>
18+
*/
19+
20+
package org.restcomm.connect.commons.util;
21+
22+
public class ClientLoginConstrains {
23+
private static char[] NOT_ALLOWED_CHARS = {'?', '=', '@'};
24+
25+
public static boolean isValidClientLogin (final String login) {
26+
for (char ch: ClientLoginConstrains.NOT_ALLOWED_CHARS) {
27+
if (login.indexOf(ch) > -1) {
28+
return false;
29+
}
30+
}
31+
return true;
32+
}
33+
}

restcomm/restcomm.docs/sources-asciidoc/src/main/asciidoc/api/calls-api.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ You must first create a RestComm client. In the example below, the Restcomm clie
125125
curl -X POST https://ACae6e420f425248d6a26948c17a9e2acf:77f8c12cc7b8f8423e5c38b035249166@127.0.0.1:8080/restcomm/2012-04-24/Accounts/ACae6e420f425248d6a26948c17a9e2acf/Calls.json -d "From=+16175551212" -d "To=client:alice" -d "Url=http://127.0.0.1:8080/restcomm/demos/hello-play.xml"
126126
....
127127

128+
This Client URI may contain URI params to declare propietary SIP headers to be
129+
included in the outgoing INVITE message.
130+
131+
----
132+
curl -X POST https://ACae6e420f425248d6a26948c17a9e2acf:77f8c12cc7b8f8423e5c38b035249166@127.0.0.1:8080/restcomm/2012-04-24/Accounts/ACae6e420f425248d6a26948c17a9e2acf/Calls.json -d "From=+16175551212" -d "To=client:alice?X-Custom-Header1=1234&X-Custom-Header2=432" -d "Url=http://127.0.0.1:8080/restcomm/demos/hello-play.xml"
133+
----
134+
128135
== Calling a DID number
129136

130137
The above example shows how to make a call to a SIP number. If you want to make a call to a DID number, you must can connect Restcomm to a DID provisioning service provider. The quickest way is to use RestComm AMI on Amazon Cloud. Get a list of all available calls. This will return all the available calls linked to the account SID

restcomm/restcomm.docs/sources-asciidoc/src/main/asciidoc/api/clients-api.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The CDR records generated for calls made by Clients, will have account SID the a
2929
|FriendlyName |A friendly name for this client.
3030
|AccountSid |The unique id of the Account that owns this phone number.
3131
|ApiVersion |Calls to this phone number will create a new RCML session with this API version.
32-
|Login |The name that is used inside the <Client> noun. This is also used by the user agent as the user name used for registration and outbound dialing.
32+
|Login |The name that is used inside the <Client> noun. This is also used by the user agent as the user name used for registration and outbound dialing. This must conform to "userinfo" part as defined in RFC2396.
3333
|Password |The password used by the user agent during registration and outbound dialing.
3434
|Status |The client status the possible values are 0 for disabled and 1 for enabled.
3535
|VoiceUrl |The URL RestComm will request when this client makes an outbound call.

restcomm/restcomm.docs/sources-asciidoc/src/main/asciidoc/rcml/client-rcml.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ Client's name with `<Video>` noun nesting.
3737

3838
For more information, please read <<video-rcml.adoc#video,Video noun documentation>>.
3939

40+
This attribute may contain URI params to declare propietary SIP headers to be
41+
included in the outgoing INVITE message.
42+
43+
----
44+
<Client>alice?X-Custom-Header1=1234&X-Custom-Header2=4321</Client>
45+
----
46+
4047
==== url
4148

4249
The 'url' attribute allows you to specify a url for a RCML document that will run on the called party's end, after she answers, but before the parties are connected. You can use this RCML to privately play or say information to the called party, or provide a chance to decline the phone call using <Gather> and <Hangup>. The current caller will continue to hear ringing while the RCML document executes on the other end. RCML documents executed in this manner are not allowed to contain the <Dial> verb. method

restcomm/restcomm.docs/sources-asciidoc/src/main/asciidoc/rcml/number-rcml.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ When the call progress events are fired, the Status Callback request also passes
7474
|SequenceNumber |The order in which the events were fired, starting from `0`. Although events are fired in order, they are made as separate HTTP requests and there is no guarantee they will arrive in the same order.
7575
|===================================================================================================================================================================================================================================================================
7676

77+
=== Including propietary SIP headers
78+
This noun allows to customize propietary SIP headers to be included in the outgoing
79+
SIP message. Those will be include following SIP URI params syntax. See example below.
80+
7781
=== Examples
7882
For an example of how to use the *<Number>* noun see below.
7983

@@ -83,4 +87,14 @@ For an example of how to use the *<Number>* noun see below.
8387
<Number sendDigits="wwww1234">1-444-555-6666</Number>
8488
</Dial>
8589
</Response>
86-
----
90+
----
91+
92+
Dialing with propietary SIP headers
93+
----
94+
<Response>
95+
<Dial>
96+
<Number sendDigits="wwww1234">1-444-555-6666?X-Custom-Header1=1234&X-Custom-Header2=4321</Number>
97+
</Dial>
98+
</Response>
99+
----
100+

restcomm/restcomm.http/src/main/java/org/restcomm/connect/http/AccountsEndpoint.java

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,13 @@
2525
import com.sun.jersey.core.header.LinkHeader.LinkHeaderBuilder;
2626
import com.sun.jersey.core.util.MultivaluedMapImpl;
2727
import com.thoughtworks.xstream.XStream;
28-
import java.net.URI;
29-
import java.util.ArrayList;
30-
import java.util.List;
31-
import java.util.Map;
32-
import javax.annotation.PostConstruct;
33-
import javax.servlet.ServletContext;
34-
import javax.servlet.http.HttpServletRequest;
35-
import javax.ws.rs.WebApplicationException;
36-
import javax.ws.rs.core.MediaType;
37-
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
38-
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
39-
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
40-
import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE;
41-
import javax.ws.rs.core.MultivaluedMap;
42-
import javax.ws.rs.core.Response;
43-
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
44-
import static javax.ws.rs.core.Response.Status.CONFLICT;
45-
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
46-
import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED;
47-
import static javax.ws.rs.core.Response.ok;
48-
import static javax.ws.rs.core.Response.status;
49-
import javax.ws.rs.core.UriInfo;
5028
import org.apache.commons.configuration.Configuration;
5129
import org.apache.shiro.crypto.hash.Md5Hash;
5230
import org.joda.time.DateTime;
5331
import org.restcomm.connect.commons.configuration.RestcommConfiguration;
5432
import org.restcomm.connect.commons.configuration.sets.RcmlserverConfigurationSet;
5533
import org.restcomm.connect.commons.dao.Sid;
34+
import org.restcomm.connect.commons.util.ClientLoginConstrains;
5635
import org.restcomm.connect.dao.ClientsDao;
5736
import org.restcomm.connect.dao.DaoManager;
5837
import org.restcomm.connect.dao.IncomingPhoneNumbersDao;
@@ -67,8 +46,6 @@
6746
import org.restcomm.connect.dao.entities.RestCommResponse;
6847
import org.restcomm.connect.extension.api.ApiRequest;
6948
import org.restcomm.connect.extension.controller.ExtensionController;
70-
import static org.restcomm.connect.http.ProfileEndpoint.PROFILE_REL_TYPE;
71-
import static org.restcomm.connect.http.ProfileEndpoint.TITLE_PARAM;
7249
import org.restcomm.connect.http.client.rcmlserver.RcmlserverApi;
7350
import org.restcomm.connect.http.client.rcmlserver.RcmlserverNotifications;
7451
import org.restcomm.connect.http.converter.AccountConverter;
@@ -82,6 +59,34 @@
8259
import org.restcomm.connect.provisioning.number.api.PhoneNumberProvisioningManager;
8360
import org.restcomm.connect.provisioning.number.api.PhoneNumberProvisioningManagerProvider;
8461

62+
import javax.annotation.PostConstruct;
63+
import javax.mail.internet.AddressException;
64+
import javax.mail.internet.InternetAddress;
65+
import javax.servlet.ServletContext;
66+
import javax.servlet.http.HttpServletRequest;
67+
import javax.ws.rs.WebApplicationException;
68+
import javax.ws.rs.core.MediaType;
69+
import javax.ws.rs.core.MultivaluedMap;
70+
import javax.ws.rs.core.Response;
71+
import javax.ws.rs.core.UriInfo;
72+
import java.net.URI;
73+
import java.util.ArrayList;
74+
import java.util.List;
75+
import java.util.Map;
76+
77+
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
78+
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
79+
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
80+
import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE;
81+
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
82+
import static javax.ws.rs.core.Response.Status.CONFLICT;
83+
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
84+
import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED;
85+
import static javax.ws.rs.core.Response.ok;
86+
import static javax.ws.rs.core.Response.status;
87+
import static org.restcomm.connect.http.ProfileEndpoint.PROFILE_REL_TYPE;
88+
import static org.restcomm.connect.http.ProfileEndpoint.TITLE_PARAM;
89+
8590
/**
8691
* @author quintana.thomas@gmail.com (Thomas Quintana)
8792
* @author maria-farooq@live.com (Maria Farooq)
@@ -800,6 +805,27 @@ private void validate(final MultivaluedMap<String, String> data) throws NullPoin
800805
} else if (!data.containsKey("Password")) {
801806
throw new NullPointerException("Password can not be null.");
802807
}
808+
809+
String emailAddress = data.getFirst("EmailAddress");
810+
try {
811+
InternetAddress emailAddr = new InternetAddress(emailAddress);
812+
emailAddr.validate();
813+
} catch (AddressException ex) {
814+
String msg = String.format("Provided email address %s is not valid",emailAddress);
815+
if (logger.isDebugEnabled()) {
816+
logger.debug(msg);
817+
}
818+
throw new IllegalArgumentException(msg);
819+
}
820+
821+
String clientLogin = data.getFirst("EmailAddress").split("@")[0];
822+
if (!ClientLoginConstrains.isValidClientLogin(clientLogin)) {
823+
String msg = String.format("Login %s contains invalid character(s) ",clientLogin);
824+
if (logger.isDebugEnabled()) {
825+
logger.debug(msg);
826+
}
827+
throw new IllegalArgumentException(msg);
828+
}
803829
}
804830

805831
}

restcomm/restcomm.http/src/main/java/org/restcomm/connect/http/CallsEndpoint.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,10 @@ private void normalize(final MultivaluedMap<String, String> data) throws Illegal
311311
throw new IllegalArgumentException(exception);
312312
}
313313
}
314-
final String to = data.getFirst("To");
314+
String to = data.getFirst("To");
315+
if (to.contains("?")) {
316+
to = to.substring(0, to.indexOf("?"));
317+
}
315318
// Only try to normalize phone numbers.
316319
if (to.startsWith("client")) {
317320
if (to.split(":").length != 2) {
@@ -336,11 +339,11 @@ protected Response putCall(final String accountSid, final MultivaluedMap<String,
336339
} catch (final IllegalArgumentException exception){
337340
return status(INTERNAL_SERVER_ERROR).entity(buildErrorResponseBody(exception.getMessage(),responseType)).build();
338341
}
342+
339343
secure(daos.getAccountsDao().getAccount(accountSid), "RestComm:Create:Calls");
344+
340345
try {
341346
validate(data);
342-
if (normalizePhoneNumbers)
343-
normalize(data);
344347
} catch (final RuntimeException exception) {
345348
return status(BAD_REQUEST).entity(exception.getMessage()).build();
346349
}
@@ -354,14 +357,30 @@ protected Response putCall(final String accountSid, final MultivaluedMap<String,
354357
statusCallbackEvent.add("completed");
355358

356359
final String from = data.getFirst("From").trim();
357-
final String to = data.getFirst("To").trim();
360+
String to = data.getFirst("To").trim();
358361
final String username = data.getFirst("Username");
359362
final String password = data.getFirst("Password");
360363
Integer timeout = getTimeout(data);
361364
timeout = timeout != null ? timeout : 30;
362365
final Timeout expires = new Timeout(Duration.create(60, TimeUnit.SECONDS));
363366
final URI rcmlUrl = getUrl("Url", data);
364367

368+
String customHeaders = getCustomHeaders(to);
369+
370+
if (customHeaders != null && !to.contains("@")) {
371+
to = (customHeaders != null) ? to.substring(0, to.indexOf("?")) : to;
372+
data.remove(to);
373+
data.putSingle("To", to);
374+
}
375+
376+
try {
377+
if (normalizePhoneNumbers)
378+
normalize(data);
379+
} catch (RuntimeException exception) {
380+
return status(BAD_REQUEST).entity(exception.getMessage()).build();
381+
}
382+
383+
365384
try {
366385
if (data.containsKey("StatusCallback")) {
367386
statusCallback = new URI(data.getFirst("StatusCallback").trim());
@@ -380,16 +399,17 @@ protected Response putCall(final String accountSid, final MultivaluedMap<String,
380399
}
381400

382401
CreateCall create = null;
402+
383403
try {
384404
if (to.contains("@")) {
385405
create = new CreateCall(from, to, username, password, true, timeout, CreateCallType.SIP,
386-
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent);
406+
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent, null);
387407
} else if (to.startsWith("client")) {
388408
create = new CreateCall(from, to, username, password, true, timeout, CreateCallType.CLIENT,
389-
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent);
409+
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent, customHeaders);
390410
} else {
391411
create = new CreateCall(from, to, username, password, true, timeout, CreateCallType.PSTN,
392-
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent);
412+
accountId, null, statusCallback, statusCallbackMethod, statusCallbackEvent, customHeaders);
393413
}
394414
create.setCreateCDR(false);
395415
if (callManager == null)
@@ -462,6 +482,16 @@ protected Response putCall(final String accountSid, final MultivaluedMap<String,
462482
}
463483
}
464484

485+
private String getCustomHeaders(final String to) {
486+
String customHeaders = null;
487+
488+
if (to.contains("?")) {
489+
customHeaders = to.substring(to.indexOf("?")+1, to.length());
490+
}
491+
492+
return customHeaders;
493+
}
494+
465495
// Issue 139: https://bitbucket.org/telestax/telscale-restcomm/issue/139
466496
@SuppressWarnings("unchecked")
467497
protected Response updateCall(final String sid, final String callSid, final MultivaluedMap<String, String> data, final MediaType responseType) {

restcomm/restcomm.http/src/main/java/org/restcomm/connect/http/ClientsEndpoint.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.apache.commons.configuration.Configuration;
4343
import org.restcomm.connect.commons.annotations.concurrency.NotThreadSafe;
4444
import org.restcomm.connect.commons.dao.Sid;
45+
import org.restcomm.connect.commons.util.ClientLoginConstrains;
4546
import org.restcomm.connect.dao.AccountsDao;
4647
import org.restcomm.connect.dao.ClientsDao;
4748
import org.restcomm.connect.dao.DaoManager;
@@ -252,9 +253,13 @@ private void validate(final MultivaluedMap<String, String> data) throws RuntimeE
252253
} else if (!data.containsKey("Password")) {
253254
throw new NullPointerException("Password can not be null.");
254255
}
255-
// https://github.com/RestComm/Restcomm-Connect/issues/1979
256-
if (data.getFirst("Login").contains("@")) {
257-
throw new IllegalArgumentException("Login contains invalid character: @ "+data.getFirst("Login"));
256+
// https://github.com/RestComm/Restcomm-Connect/issues/1979 && https://telestax.atlassian.net/browse/RESTCOMM-1797
257+
if (!ClientLoginConstrains.isValidClientLogin(data.getFirst("Login"))) {
258+
String msg = String.format("Login %s contains invalid character(s)",data.getFirst("Login"));
259+
if (logger.isDebugEnabled()) {
260+
logger.debug(msg);
261+
}
262+
throw new IllegalArgumentException(msg);
258263
}
259264
}
260265

restcomm/restcomm.http/src/main/java/org/restcomm/connect/http/UssdPushEndpoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ protected Response putCall(final String accountSid, final MultivaluedMap<String,
122122
//Currently we don't support StatusCallback for USSD Push requests
123123
try {
124124
create = new CreateCall(from, to, username, password, true, timeout, CreateCallType.USSD,
125-
accountId, null, null, null, null);
125+
accountId, null, null, null, null, null);
126126
create.setCreateCDR(false);
127127
Future<Object> future = (Future<Object>) ask(ussdCallManager, create, expires);
128128
Object object = Await.result(future, Duration.create(10, TimeUnit.SECONDS));

0 commit comments

Comments
 (0)