三新专号服务程序应用程序接口文档¶
构建时间:2015-11-19 06:39:15
內容目录¶
三新专号服务程序应用程序接口文档¶
readthedocs 构建状态:
什么是三新专号¶
todo: 什么是三新专号 ... ...
关于该文档¶
该文档规定了三新专号系统中,服务程序与客户端程序(包括且不限于App,WebApp)之间的程序接口。 服务器程序与客户端开发者需要遵照该文档的规定进行程序开发工作。
其设计的根据是三新专号 App 的界面原型 https://modao.cc/app/POxerAK2EbEA7gmDx2GT
原则¶
- 基于 HTTP 1.1 ( HTTP 2.0 就算了吧 -_-)的 WebAPI
- 客户端 -> 服务器 单方向访问
- Restfule API 形态
- 使用 UTF8 编码的 JSON 记录结构化数据
- 使用 HTTPS
HTTP Content¶
POST 或者 PUT 请求采用 JSON 格式的数据,
此时 Content-Type 头域的值应是 application/json
回复的 Content 也必须是 JSON 格式,
其 Content-Type 头域的值应是 application/json
。
如果不需要返回数据,为了保证 Content 可以被 JSON 解码,应使用 null
作为回复的内容,例如:
HTTP/1.1 200 OK
Content-Type: application/json
null
HTTP Status Code¶
500 执行失败¶
如果服务器在响应API调用期间出现错误,应返回 500 Internal Server Error。
如果服务器可以提供具体的错误编码以及错误信息,应采用 JSON 格式在内容中记录这些数据:
code
属性记录错误编码, text
属性记录错误文本信息。
如:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{"code": 10013, "text": "calee not allowed"}
注意
服务器无法在所有情况在都提供 JSON 格式错误信息,客户端应注意判断。
401 授权错误¶
登录失败, API 请求签名验证错误,均返回该 Status Code
安全¶
User API 安全¶
当APP向发起HTTP请求时, 必须 附带若干URL参数,作为验证依据。
后台服务在收到请求时,会进行验证,并拒未通过绝验证的请求。
-
ANY
/api/user/(string: telnum)/*
¶ 该系统的服务器需要为每一个接入程序(以下简称 APP )分配相应的 ACCESS-ID 和 ACCESS-KEY。
APP 在调用服务器接口时,需要使用 ACCESS-ID 和 ACCESS-KEY 进行签名。 服务器会验证签名的正确性,并拒绝验证不通过的请求。
使用URL参数承载签名数据。
Query Parameters: - accessid (string) – 客户端访问ID,由系统管理员分配
- timestamp (string) –
Unix 时间戳,形如: 1445851008
注意
服务器将验证时间戳,如果与服务器时间差超过正负48小时,服务器将认为验证失败
- signature (string) – 签名,由 url path, telnum, password , token (见
POST /api/user/(string: telnum)/login
) , timestamp , accessid, accesskey 7个参数计算得出
APP应将
signature
参数填写为 SHA1 哈希值的16进制字符串表达式,其算法是:- 将 url path (裁剪结尾的 / ), telnum, password (大写MD5散列HEX表达式) , token, timestamp, accessid, accesskey (大写MD5散列HEX表达式) 这7个参数按照字符串顺序进行从小到大的排序。
- 将排序完毕的各个参数字符串头尾相连,连接成新的字符串。
- 计算上一个步骤中,连接好的字符串的 SHA-1 散列值字符串,这个字符串是经过大写字母转换(upper case)的十六进制表达式。
注意
调用登录接口时(
POST /api/user/(string: telnum)/login
), token 参数应使用空字符串
签名算法例子代码¶
在下面的例子中:
手机号码为 13887654321 的用户已经登录;
其登录密码是 This_Is#My&p@ssw0rd;
该APP的访问ID与KEY分别是 developer-001 与 xm90uojWSd34E8y3 ;
其 token=4C609E5D5D234A406D446EA42898EFAD50E4541C ;
当前时间戳是 1407812629434 ;
要访问的URL路径是 /api/user/13887654321/path/of/the/api ;
那么,最终计算得出的签名将是 DCE009D2AF85050E249A6511D1C0F0F180EDFA64
加上验证和签名参数的完整url是:
http://api/user/13887654321/path/of/the/api?accessid=developer-001×tamp=1407812629434&signature=DCE009D2AF85050E249A6511D1C0F0F180EDFA64
下面是几种语言的算法实现片段:
Java¶
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
public class Signature {
public static String byteArrayToHex(byte[] byteArray) {
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F' };
char[] resultCharArray = new char[byteArray.length * 2];
int index = 0;
for (byte b : byteArray) {
resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
resultCharArray[index++] = hexDigits[b & 0xf];
}
return new String(resultCharArray);
}
public static String hashStr(String input, String digest)
throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance(digest);
byte[] inputByteArray = input.getBytes();
messageDigest.update(inputByteArray);
byte[] resultByteArray = messageDigest.digest();
return byteArrayToHex(resultByteArray);
}
public static String calcSignature() throws NoSuchAlgorithmException {
String accessid = "developer-001";
String accesskey = hashStr("xm90uojWSd34E8y3", "MD5");
String urlpath = "/api/user/13887654321/path/of/the/api";
String telnum = "13887654321";
String token = "4C609E5D5D234A406D446EA42898EFAD50E4541C";
String password = hashStr("This_Is#My&p@ssw0rd", "MD5");
String timestamp = "1407812629434";
ArrayList<String> tmpList = new ArrayList<String>();
tmpList.add(accessid);
tmpList.add(accesskey);
tmpList.add(urlpath);
tmpList.add(telnum);
tmpList.add(token);
tmpList.add(password);
tmpList.add(timestamp);
Collections.sort(tmpList);
String result = hashStr(String.join("", tmpList), "SHA1");
return result;
}
public static void main(String[] args) {
try {
String sigstr = calcSignature();
System.out.format("Signature = %s", sigstr);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
NodeJs¶
var crypto = require('crypto');
(function (){
let hashStr = function (input, algorithm) {
let hasher = crypto.createHash(algorithm);
hasher.update(input);
return hasher.digest('hex').toUpperCase();
}
let accessid = "developer-001";
let accesskey = hashStr("xm90uojWSd34E8y3", "md5");
let urlpath = "/api/user/13887654321/path/of/the/api";
let telnum = "13887654321";
let token = "4C609E5D5D234A406D446EA42898EFAD50E4541C";
let password = hashStr("This_Is#My&p@ssw0rd", "md5");
let timestamp = "1407812629434";
let tmpList = [accessid,accesskey, urlpath, telnum, token, password, timestamp];
tmpList.sort();
let signature = hashStr(tmpList.join(''), 'sha1');
console.log("signature = " + signature);
})();
Php¶
<?php
$accessid = 'developer-001';
$accesskey = strtoupper(md5('xm90uojWSd34E8y3'));
$url_path = '/api/user/13887654321/path/of/the/api';
$telnum = '13887654321';
$token = '4C609E5D5D234A406D446EA42898EFAD50E4541C';
$password = strtoupper(md5('This_Is#My&p@ssw0rd'));
$timestamp = '1407812629434';
$tmp_arr = array($accessid, $accesskey, $url_path, $telnum, $token, $password, $timestamp);
sort($tmp_arr, SORT_STRING);
$signature = strtoupper(sha1(implode($tmp_arr)));
echo(signature);
Python¶
仅适用于 Python 3.0+
from hashlib import sha1, md5
accessid = b'developer-001'
accesskey = bytes(md5(b'xm90uojWSd34E8y3').hexdigest().upper(), 'ascii')
url_path = b'/api/user/13887654321/path/of/the/api'
telnum = b'13887654321'
token = b'4C609E5D5D234A406D446EA42898EFAD50E4541C'
password = bytes(md5(b'This_Is#My&p@ssw0rd').hexdigest().upper(), 'ascii')
timestamp = b'1407812629434'
signature = sha1(b''.join(sorted([accessid, accesskey, url_path, telnum, token, password, timestamp]))).hexdigest().upper()
print(signature)
获取 Unix 时间戳的例子代码¶
以下是几种常见语言获取 Unix 时间戳的方法:
C¶
#include <time.h> /* time_t, struct tm, time ... */
/// ... ...
time_t val = time(NULL);
int ts = (int) val;
/// ... ...
C#¶
int tx = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
Java¶
long ts = System.currentTimeMillis()/1000L;
Javascript¶
var ts = Math.floor(Date.now()/1000);
Php¶
<?php
$ts = time();
Python¶
import time
ts = int(time.time())
CTI API 安全¶
-
ANY
/api/cti/*
¶ CTI服务器与Web服务器之间的通信与APP的不同,它们之间的API调用是内部的、固定的、无身份的。
Web服务器 同时 使用下面两种方法保障CTI调用的安全:
SSL 客户端证书:
Web服务器要为CTI服务器颁客户端SSL证书,只有通过SSL验证的HTTP请求才被允许。 建议使用 nginx 进行SSL证书验证,见 http://nginx.org/en/docs/http/ngx_http_ssl_module.html
-
Web服务器要针对CTI的HTTP亲求进行HTTP基本认证
验证与授权¶
登录¶
-
POST
/api/user/(string: telnum)/login
¶ 手机号码为 telnum 的用户登录
Request JSON Object: - password (string) – 密码的MD5散列,用十六进制字符串(其中的小写字母转要换成大写字母)表示
Response JSON Object: - token (string) – 令牌
用于该用户所有后续请求的验证,见
ANY /api/user/(string: telnum)/*
注意
token 具有时效性。服务程序在一段时间之后,会抛弃原来的令牌。 此时,客户端需要调用登录接口,以获取新的令牌。 一点调用该接口,原来的令牌就会失效。
用户管理¶
用户管理API用于管理用户的账户信息和基本资料。
注意
在本系统中,用户使用手机号码登录,且专号与手机号码绑定,因此,手机号码被视作用户ID。
获取用户信息¶
-
GET
/api/user/(string: telnum)
¶ 返回手机号码为 telnum 的用户的基本信息
Response JSON Object: - telnum (string) – 用户的手机号码
- name (string) – 用户的名称
- createtime (string) – 用户建立的时间(ISO格式)
- avatar (string) – 用户的头像(BASE64格式)
注册新用户¶
-
POST
/api/user
¶ Request JSON Object: - telnum (string) – 用户的手机号码 (必填)
- name (string) – 用户的名称 (必填)
- avatar (string) – 用户的头像(BASE64格式) (非必填)
专号管理¶
专号管理API用于管理用户所属的专号
获取专号列表¶
-
GET
/api/user/(string: telnum)/vtelnum
¶ 返回手机号码为 telnum 的用户所拥有的专号列表
Query Parameters: - page (int) – 要返回的页码。默认为1(1开始)。
- perPage (int) – 每页长度。如果不指定,服务器采用其默认设置。
Response Headers: - X-Pagination-Current-Page – 每页长度
- X-Pagination-Per-Page – 当前返回结果的页码(1开始)
- X-Pagination-Totle-Pages – 总页数
- X-Pagination-Totle-Entries – 总条目数
Response JSON Object: - vtelnum (string) – 专号
例子¶
此例子中,返回手机号码 (telnum) 为 123 的用户,所拥有的5个专号中的第3~4个
Request
GET /api/user/123/vtelnum?page=2&perPage=2 HTTP/1.1
Host: example.com
Response
HTTP/1.1 200 OK
Content-Type: application/json
X-Pagination-Current-Page: 2
X-Pagination-Per-Page: 2
X-Pagination-Totle-Pages: 3
X-Pagination-Totle-Entries: 5
[{"vtelnum": "10003"}, {"vtelnum": "10004"}]
获取可选专号列表¶
-
GET
/api/user/(string: telnum)/availablevtelnum
¶ 返回手机号码为 telnum 的用户可以选取的可用专号的列表
Query Parameters: - page (int) – 要返回的页码。默认为1(1开始)。
- perPage (int) – 每页长度。如果不指定,服务器采用其默认设置。
Response Headers: - X-Pagination-Current-Page – 每页长度
- X-Pagination-Per-Page – 当前返回结果的页码(1开始)
- X-Pagination-Totle-Pages – 总页数
- X-Pagination-Totle-Entries – 总条目数
Response JSON Object: - vtelnum (string) – 专号
绑定新专号¶
-
POST
/api/user/(string: telnum)/vtelnum
¶ 手机号码为 telnum 的用户为该手机号码绑定一个新的专号
Request JSON Object: - vtelnum (string) – 要绑定的新专号
电话控制¶
发起呼叫¶
-
POST
/api/user/(string: telnum)/makecall
¶ 手机号码为 telnum 的用户通知服务器,将要发起呼叫
Request JSON Object: - caller (string) – 作为主叫号码专号
- callee (string) – 被叫号码
Response JSON Object: - callid (string) – 呼叫ID
注解
调用用该API之后,服务器将记录 telnum 的呼叫请求。
在 2分钟 之内,服务器如果收到 telnum 向其专号 caller 的呼叫, 就会自动接通,然后以 caller 为主叫号码,向 callee 发起呼叫,并桥接两者之间的通话。
注意
对于同一个手机号码,服务器只保存其最近一次呼叫请求。 如果同一个手机号码连续调用该接口,服务器将以最后一次请求为准。
注意
如果没有调用该接口,或者调用后又取消(POST /api/user/(string: telnum)/cancelcall
),
服务器不会接受 telnum 对 caller 的电话呼叫。
例子¶
此例中,电话号码为 1001 的用户,专号是 2001 ,他使用该专号呼叫目标号码 3001
Request
POST /api/user/1001/makecall HTTP/1.1
Host: example.com
Content-Type: application/json
{"caller": "2001", "callee": "3001"}
Response
HTTP/1.1 200 OK
Content-Type: application/json
{"callid": "12870498123"}
电话服务¶
电话网服务器(CTI)通过调用Web服务器的电话服务接口,控制其电话线路的接续、转接等行为。
注意
这一系列的接口需要进行 HTTP 基本认证
呼入事件¶
-
POST
/api/cti/callin
¶ 电话网服务器(CTI)在接到电话呼入后,调用这个API。 Web服务器应在API的返回中,告知CTI服务器后续动作。
Request JSON Object: - from (string) – 主叫号码
- to (string) – 被叫号码
Response JSON Object: - action (string) –
CTI后续动作,有:
- bridge : 桥接
- refuse : 拒接
- caller (string) –
进行桥接时的主叫号码
注解
仅当 action 为 “bridge” 时有效
- callee (string) –
进行桥接时的被叫号码
注解
仅当 action 为 “bridge” 时有效
例子¶
此例中,电话号码为 1001 的用户,专号是 2001 ,他使用该专号呼叫目标号码 3001。
当APP的呼叫发起请求 (POST /api/user/(string: telnum)/makecall
) 提交成功后,
APP控制手机呼叫 2001 。
此时,CTI服务器询问Web服务器后续动作,而Web服务器告知CTI应以 2001 的名义呼叫 3001 ,
并桥接 1001 与 3001 :
Request
POST /api/cti/callin HTTP/1.1
Host: example.com
Content-Type: application/json
{"from": "1001", "to": "2001"}
Response
HTTP/1.1 200 OK
Content-Type: application/json
{"action": "bridge", "caller": "2001", "callee": "3001"}