加密通信软件Signal 2.92版本编译安装折腾手记(Ubuntu 18.04)

前言

加密通信软件Signal是开源的,安全性很高,号称斯诺登也推荐大家使用。既然这么好,那必然会有不少人去尝试复制修改这个软件。但是有个很大的问题,这个软件的官网和github仓库中都没有看到安装文档,尝试的人基本上都是铩羽而归。据Reddit上有人说联系过Signal的开发人员,开发人员说他们的精力要放在优先级更高的事情上,所以顾不上安装部署文档。在我看来,Signal的意思是“只要我放出来全套源代码来让大家检查,对我的安全性有信心就行。要想复制我,没门,或者代价高”(但也可能真的人手不够)。Reddit原帖在此

顺带说一下,另一款加密通信软件“密信”也是开源的,但是貌似没有看到其REST API服务器的代码。

既然部署Signal服务器的难度很高,那基本上也就没什么人去给它的服务端贡献代码了,因为没法调试、测试,怎么能发起有效的PR?所以Signal的Desktop/iOS/Android客户端提供了开发和测试文档,让人能给客户端贡献代码。不过这个指南不是在客户端的README.md中,而是在CONTRIBUTING.md中,这是我先是试图用npm编译它的Desktop客户端、发现编不过去后才注意到的,得用yarn编译。

编译

言归正传,继续说Signal服务端。首先,Signal Server目前的代码是2.92版本,既然有工程文件pom.xml,那就用Maven编译好了。仓库地址是https://github.com/signalapp/Signal-Server/

mvn install -DskipTests

编译这个版本的代码需要用JDK 11+(Ubuntu 18.04默认的版本就是11),如果用低版本的JDK(比如Ubuntu 16.04中的),Maven会报一个和数字11有关的错误,如果不熟悉的话,这个看似莫名其妙的错误信息可能会让你挠头半天,算一个坑。所以折腾的环境定为Ubuntu 18.04。编译的最终产物是TextSecureServer-2.92.jar。

寻找别人的安装经验指南

编译好了,接下来要部署了。先谷歌一下关键字Signal Sever + Install/Deploy,引用最多的是一篇1.88版本的安装指南:《Signal Server Installation Guide》。看下面的评论,有人说此人是骗子,因为省略了很多步骤和细节,无法依葫芦画瓢。注意看这个指南的最后一句,作者说自己是个自由职业者,有问题可以邮件联系他。猜测这位同学可能是要用这个帖子做导流来收取一些咨询费的,所以没写那么细。但在没有官方文档的情况下,这个指南也是一个很好的开始了(此处掌声致谢),只能先按这个搞。

配置的总入口:模板文件sample.yml

把Signal Server代码仓库中的配置模板文件sample.yaml拿出来,改名为config.yml,按上面这个指南配置,会发现2.92比1.88的配置项有了一些变化(那是自然)。后面的事实会证明,代码仓库中的这个yml模板,是很老的版本,和代码根本不匹配,照这个配几乎无用。

第三方接口的帐号申请

先搜罗一下yml中需要申请的第三方接口的帐号,挨个申请。
要申请的帐号清单:

  • Twilio           用于短信等功能。messagingServicesId的申请页面和account的页面不在一起,万一没找到,messagingServicesId先填个假的。
  • S3               亚马逊S3,用于图床CDN、聊天的附件等。
  • SQS             亚马逊SQS,消息队列。
  • reCAPTCHA  谷歌reCAPTCHA,用于识别机器人。
  • GCM            谷歌推送服务。谷歌早已从GCM切换到FCM,无法再申请到GCM帐号。试了一下用谷歌FCM的帐号填写在那里也没报错,但没实际测试对安卓手机的推送功能是否正常。
  • APN             苹果推送服务。

配置服务器的域名

没有域名的话,买一个。HTTPS/WSS(WebSocket Secure)服务都是需要校验域名对应的证书的,Twilio服务的反向连接也需要配置域名。

安装Redis

sudo apt-get install -y redis-server

修改配置文件:

sudo vi /etc/redis/redis.conf

1、把 supervised no 改为 supervised systemd

2、把 bind 127.0.0.1 ::1 改为 bind 127.0.0.1

重启Redis:

sudo systemctl restart redis.service

安装PostgreSQL

安装数据库软件,创建数据库,创建帐号:

sudo apt-get install postgresql postgresql-contrib -y
sudo -u postgres psql -c "CREATE DATABASE accountdb"
sudo -u postgres psql -c "CREATE DATABASE messagedb"
sudo -u postgres psql -c "CREATE DATABASE abusedb"
sudo -u postgres psql -c "CREATE DATABASE keysdb"
sudo -u postgres psql -c "CREATE DATABASE readdb"
sudo -u postgres psql -c "CREATE USER signal \
WITH ENCRYPTED PASSWORD 'password'"

修改数据库配置:

sudo vi /etc/postgresql//main/postgresql.conf

1、将 listen_addresses='localhost'  改为  listen_addresses='*'

2、启用数据库的密码认证。尾部追加一行,内容为:
     host all all * md5

重启数据库:

sudo systemctl restart postgresql

安装coturn

TURN协议是给NAT打洞,让聊天信息能穿透到内网的。coturn是TURN的一个实现。

安装coturn并生成证书:

sudo apt-get install -y coturn
sudo openssl req -x509 -newkey rsa: \
-keyout /etc/turn_server_pkey.pem \
-out /etc/turn_server_cert.pem -days -nodes

生成证书时要输入服务器的完整域名。

编辑配置文件:

sudo vi /etc/turnserver.conf

配置下面的字段:

listening-ip=
external-ip=
use-auth-secret
static-auth-secret=
user=user1:password1
realm=服务器的完整域名
cert=/etc/turn_server_cert.pem
pkey=/etc/turn_server_pkey.pem
cli-password=

配置好之后,重启coturn服务,在防火墙上对外开放coturn的端口(默认是3478端口),然后用下面的页面测试coturn的基本功能:

https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

注意:coturn有两套认证机制,一套是用户名、密码的,一套是共享密钥(secret)。其中共享密钥是给REST接口使用的,Signal Server要用到共享密钥。而测试coturn基本功能时使用的页面只支持用户名+密码的认证方式,而且coturn是优先使用共享密钥的,也就是说如果同时配了共享密钥、用户名+密码的话,在上面这个测试页面中是测试不过的。所以要先禁用共享密钥的设置,等在上面的这个页面中测试通过后,再把共享密钥的配置恢复。

配置好共享密钥后,启动daemon:

sudo turnserver -a -o -v -n -u test:test -r "服务器的完整域名"

安装Apache2

在防火墙上对外开放服务器的80端口,因为获取免费证书时需要回连80端口来验证。

下面用a2enmod打开需要的几种Apache代理模块,用certbot获取域名对应的免费HTTPS证书,最后会提示将80端口重定向到443端口。

sudo install apache2
sudo a2enmod proxy proxy_http proxy_wstunnel
sudo add-apt-repository ppa:certbot/certbot -y
sudo apt-get update -y
sudo apt-get install python-certbot-apache -y
sudo certbot --authenticator standalone --installer apache -d "你的服务器完整域名" --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2"

编辑Apache的HTTPS站点的配置文件:

sodu vi /etc/apache2/sites-available/-default-le-ssl.conf

设置反向代理,将Apache的443端口来的客户端请求反向代理到Signal Server的8080端口。443的wss://反向代理到8080的ws://,443的https://反向代理到8080的http://。Signal Server除了用8080端口来监听ws://和http://,还会起一个8081的对内管理端口,可以用浏览器打开http://127.0.0.1:8081查看。

HTTPS站点的配置样本如下:

<IfModule mod_ssl.c>
<VirtualHost *:>
  ServerAdmin webmaster@excmple.com
  DocumentRoot /var/www/html   AllowEncodedSlashes NoDecode   RewriteEngine on
  RewriteCond %{HTTP:Upgrade} websocket [NC]
  RewriteCond %{HTTP:Connection} upgrade [NC]
  RewriteRule .* "ws://localhost:8080$0" [P,L]   ProxyRequests off
  ProxyPass / http://localhost:8080/
  ProxyPassReverse / http://localhost:8080/   #LogLevel trace6   ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined   ServerName 你的服务器完整域名
  SSLCertificateFile /etc/letsencrypt/live/你的服务器完整域名/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/你的服务器完整域名/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

其中HTTPS证书是Signal客户端也需要用到的。

上述反向代理的规则是通过谷歌抄自"How to Reverse Proxy Websockets with Apache 2.4"。这个配置首先用RewriteEngine匹配客户端来的wss://协议中的协议提升字段,匹配上的就转发到ws://,没匹配的用ProxyPass反向代理到http://。但是原始帖子中的配置有三处错误导致Signal的客户端和服务器的通信不成功,此处给出的是已经踩坑修正过的(有兴趣的可以自行对比一下错误在哪里)。

此处既然遇到问题,就需要用到码农的两大利器:日志、调试器。通过修改上述站点的日志配置字段为LogLevel trace6来打开Apahe的详细日志并重启Apache,进而分析/var/log/access.log、/var/log/error.log来确定是Apache的问题,还是8080端口的Signal Server的问题。

AllowEncodedSlashes NoDecode这句就是根据日志分析出来要加上的,因为iOS客户端发给Signal Server的URI中包含特殊字符如斜杠%2F、加号%2B等,如果没有这句的话,Apache可能直接对这些URI返回404,并不会转发到Signal Server的8080端口。

安装CDS

Signal的CDS(联系人发现服务)对于部署者应该是个大坑。为什么这么说呢?CDS一般并不和Signal Server部署在一台机器上,而是使用独立的特殊硬件来保证其安全性,也就是Intel SGX。SGX能创建硬件隔离区域,称为enclave(中文名叫“飞地”)。目前云服务提供商中只有微软Azure和Intel合作提供了云上的SGX的试用环境,而且有地理区域限制。如果搞不到Azure的SGX环境,只能购买支持SGX的Intel机器。这次我没有配置SGX,而是在config.yml中配置了假数据。

有兴趣的可以参考折腾过CDS的人写的指南,这个指南也不再维护了:https://github.com/on-premise-signal/signal-setup-guide
CDS的代码仓库是:https://github.com/signalapp/ContactDiscoveryService

Signal Server的处女Run

初始化数据库,建表(此处暂未深究为什没有处理名为readdb的数据库):

java -jar TextSecureServer-2.92.jar  messagedb  migrate  config/config.yml
java -jar TextSecureServer-2.92.jar accountdb migrate config/config.yml
java -jar TextSecureServer-2.92.jar keysdb migrate config/config.yml
java -jar TextSecureServer-2.92.jar abusedb migrate config/config.yml

填满config.yml的各个字段后,尝试运行Signal Server:

java -jar ./service/target/TextSecureServer-2.92.jar server config/config.yml

结果不出意料:是跑不起来的。好在打印的错误信息明确指明了是哪个配置项有问题,只能去抠代码了(因为Signal的开发人员都说了,有代码不就有了一切嘛),从此要开始“看错误信息 -> 谷歌搜错误/看代码/调代码 -> 改配置”的循环,这是依赖谷歌搞开发的时代下码农的宿命,还得准备好一个比较隐蔽、速度又比较快的ladder以保证谷歌24小时在线。

新的配置模板文件config.yml

好在Signal Server的配置项解析代码是集中在service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java这个文件中的。用IDEA打开工程,翻到这个文件,找到每个配置项的类型声明和解析代码,统一扒出来,做一个新的config.yml模板:

twilio:
  accountId:
  accountToken:
  numbers: # array
  localDomain:
  messagingServicesId: push:
  queueSize: 200 attachments:
  accessKey:
  accessSecret:
  bucket:
  region: cdn:
  accessKey:
  accessSecret:
  bucket:
  region: cache:
  url:
  replicaUrls: # array directory:
  redis:
    url:
    replicaUrls: # array
  sqs:
    accessKey:
    accessSecret:
    queueUrl:
    region: us-east-1
  client:
    userAuthenticationTokenSharedSecret: # hex string
    userAuthenticationTokenUserIdSecret: # hex string
  server:
    replicationUrl:
    replicationPassword:
    replicationCaCertificate: accountDatabaseCrawler:
  chunkSize: 1000
  chunkIntervalMs: 8000 pushScheduler:
  url:
  replicaUrls: # array messageCache:
  redis:
    url:
    replicaUrls: # array
  persistDelayMinutes: 10 messageStore:
  driverClass: org.postgresql.Driver
  user:
  password:
  url: # example: jdbc:postgresql://127.0.0.1:5432/messagedb abuseDatabase:
  driverClass: org.postgresql.Driver
  user:
  password:
  url: # example: jdbc:postgresql://127.0.0.1:5432/abusedb testDevices: # array: [ { "number" => "", "code" => "" }, { "number" => "", "code" => "" }, ... ] maxDevices: # array: [ { "number" => "", "count" => "" }, { "number" => "", "count" => "" }, ... ] keysDatabase:
  driverClass: org.postgresql.Driver
  user:
  password:
  url: # example: jdbc:postgresql://127.0.0.1:5432/keysdb accountsDatabase:
  driverClass: org.postgresql.Driver
  user:
  password:
  url: # example: jdbc:postgresql://127.0.0.1:5432/accountdb read_database:
  driverClass: org.postgresql.Driver
  user:
  password:
  url: # example: jdbc:postgresql://127.0.0.1:5432/readdb #limits: #use default values in source code #httpClient: #use default values in source code #webSocket: #use default values in source code turn:
  secret:
  uris: # array gcm:
  senderId:
  apiKey: apn:
  pushCertificate:
  pushKey:
  bundleId:
  sandbox: false unidentifiedDelivery:
  certificate:
  privateKey:
  expiresDays: voiceVerification:
  url:
  locales: # array recaptcha:
  secret: storageService:
  userAuthenticationTokenSharedSecret: # hex string backupService:
  userAuthenticationTokenSharedSecret: # hex string transparentDataIndex: # Map<String, String> #server: #use default values in source code
#logging: #use default values in source code
#metrics: #use default values in source code

重新把这个config.yml的各个字段填满,别高兴太早,依然是跑不起来的,因为有些字段有严格的要求,是在代码中通过Java标注来要求的。一开始肯定是想当然地按自己的字面理解去填写的,无法满足要求。

比如testDevices这个配置项,在WhisperServerConfiguration.java中是这么写的:

@Valid
@NotNull
@JsonProperty
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();

说明testDevices是json格式,代码直接给new了一个空集作为默认值,可以不配置,但是如果你在yml文件中配置了这项的话,一定要有值,也就是要给它配上至少一个{number, code}对。所以干脆在yml中注释掉它好了。maxDevices等配置项也类似。所以为什么说在一开始捋这些字段时就要注意一下Java标注和对应的使用代码。

再比如storageService和backupService下面的userAuthenticationTokenSharedSecret这个字段,解析代码如下。显然这是一个十六进制字符串格式的字段,而且不能为空串。

@NotEmpty
@JsonProperty
private String userAuthenticationTokenSharedSecret; public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException {
return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray());

配置APN证书

稍麻烦一点的配置是APN证书,先要用Apple Developer帐号申请拿到证书文件,然后把证书文件转换为PEM格式,然后把PEM文件中的字符串贴在yml文件中。注意不是在yml中配置PEM文件的路径。为什么?Read The F*cking Code! 代码中是直接读取PEM串的内容,而不是读取PEM文件,除非把代码改为从PEM文件中读取(1.88安装指南那个帖子的评论里面已经有人这么干了)。

Apple给的证书文件转换为PEM的步骤:
  文件一:aps.cer (证书文件)
  文件二:push-pro.p12 (私钥文件,可能设置有密码保护)

1、把APN证书转换为PEM格式:

openssl x509 -in aps.cer -inform der -out aps.pem

2、把APN私钥转换为PEM格式:

输入.p12文件的密码,再设定一个PEM密码,转PEM格式:

openssl pkcs12 -nocerts -out push-pro.pem -in push-pro.p12

输入上一步设定的PEM密码,去除密码:

openssl rsa -in push-pro.pem -out push-pro-key-noenc.pem

yml文件中最终配置的APN证书格式如下。APN私钥类似。

apn:
  pushCertificate: |
    -----BEGIN CERTIFICATE-----
    MIIGCDCCBPCgAwIBAgIId9kHAiU6ZxIwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNV
    (此处省略五千字。。。注意对齐)
    mYm1yVxw4lLirPPyva9YOqX626RzyKQ0mcOiV0vURjALRiAkNklfpgr877wilvdS
    dynVUqLbErSibtAh
    -----END CERTIFICATE-----

配置Sealed Sender的密钥

更麻烦一点的是unidentifiedDelivery这个功能的配置。这个功能又称为Sealed Sender,即匿名发聊天信息。功能的设计参考https://signal.org/blog/sealed-sender/。这个功能需要证书certificate、私钥privateKey,但并不是PEM等标准格式的。仍然翻一下相关的解析代码,最后会发现Signal Server自带了生成这些数据的命令行参数。具体的代码在service/src/main/java/org/whispersystems/textsecuregcm/workers/CertificateCommand.java这文件中,从这代码可以看出,需要两步走。

第一步:生成CA的证书和私钥。这个CA证书(注意不是CA的私钥)在Signal的客户端中可能也要用到,客户端访问服务器时可以走HTTPS的代理。

java -jar TextSecureServer-2.92.jar certificate --ca

第二步:利用CA的私钥,生成服务器的证书。

java -jar TextSecureServer-2.92.jar certificate --key XXX --id NNNN

其中XXX是CA的私钥,NNNN是自己决定的一个数字编号。

最终的配置文件config.yml

最后得到的config.yml的内容。用这个配置,仅仅可以跑通手机客户端的初步注册,其他很多功能仍需要调测。Signal客户端也有一些配置要修改。先搞到这里,不进一步折腾了。

twilio:
accountId: ACf1111111111111111111111111111
accountToken: 3e22222222222222222222222222
numbers:
- +1923456789
localDomain: 你的完整域名
messagingServicesId: MG3333333333333333333333333333 push:
queueSize: 200 attachments:
accessKey: AKIAAAAAABBBBBCCCCCCC
accessSecret: QhABCCCCCCCCCCCCCDDDDDDDDDDDDDDDD
bucket: fake
region: ap-southeast-1 cdn:
accessKey: AKIAAAAAABBBBBCCCCCCC
accessSecret: QhABCCCCCCCCCCCCCDDDDDDDDDDDDDDDD
bucket: fake
region: ap-southeast-1 cache:
url: http://127.0.0.1:6379
replicaUrls: [ "http://127.0.0.1:6379" ] directory:
redis:
url: http://127.0.0.1:6379
replicaUrls: [ "http://127.0.0.1:6379" ]
sqs:
accessKey: AKIAAAAAABBBBBCCCCCCC
accessSecret: QhABCCCCCCCCCCCCCDDDDDDDDDDDDDDDD
queueUrl: https://sqs.ap-southeast-1.amazonaws.com/17093374723/fake
region: ap-southeast-1
client:
userAuthenticationTokenSharedSecret: 33445566 #fake
userAuthenticationTokenUserIdSecret: 22117788 #fake
server:
replicationUrl: http://127.0.0.1 #fake
replicationPassword: fake
replicationCaCertificate: | #fake
-----BEGIN CERTIFICATE-----
MIIGCDCCBPCgAwIBAgIId9kHAiU6ZxIwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNV
(此处省略五千字。。。)
mYm1yVxw4lLirPPyva9YOqX626RzyKQ0mcOiV0vURjALRiAkNklfpgr877wilvdS
dynVUqLbErSibtAh
-----END CERTIFICATE----- accountDatabaseCrawler:
chunkSize: 1000
chunkIntervalMs: 80000000 pushScheduler:
url: http://127.0.0.1:6379
replicaUrls: [ "http://127.0.0.1:6379" ] messageCache:
redis:
url: http://127.0.0.1:6379
replicaUrls: [ "http://127.0.0.1:6379" ]
persistDelayMinutes: 10 messageStore:
driverClass: org.postgresql.Driver
user: signal
password: password
url: jdbc:postgresql://127.0.0.1:5432/messagedb abuseDatabase:
driverClass: org.postgresql.Driver
user: signal
password: password
url: jdbc:postgresql://127.0.0.1:5432/abusedb keysDatabase:
driverClass: org.postgresql.Driver
user: signal
password: password
url: jdbc:postgresql://127.0.0.1:5432/keysdb accountsDatabase:
driverClass: org.postgresql.Driver
user: signal
password: password
url: jdbc:postgresql://127.0.0.1:5432/accountdb read_database:
driverClass: org.postgresql.Driver
user: signal
password: password
url: jdbc:postgresql://127.0.0.1:5432/readdb #limits: #use default values in source code #httpClient: #use default values in source code #webSocket: #use default values in source code turn:
secret: turnsecret
uris:
- turn:127.0.0.1:3478?transport=udp gcm:
senderId: 71722022434122
apiKey: AAAfffffffffffffffffffffffffaaaaaaaaaaaaaaaaaaakkkkkkkkkkkkkkkkeeeeeeeeeeeee apn:
bundleId: fake
pushCertificate: |
-----BEGIN CERTIFICATE-----
MIIGCDCCBPCgAwIBAgIId9kHAiU6ZxIwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNV
(此处省略五千字。。。)
mYm1yVxw4lLirPPyva9YOqX626RzyKQ0mcOiV0vURjALRiAkNklfpgr877wilvdS
dynVUqLbErSibtAh
-----END CERTIFICATE-----
pushKey: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsH9uXZaGp2DZ0sfsSI9ovrv4hI2OjEDbzefv5+ZWy0kVdL/r
   (此处省略五千字。。。)
2UtkH7KXehd9wueaIaZ5+j9PKy3t6x4GPZE4b/fkuXToCijOsbCfhA==
-----END RSA PRIVATE KEY----- unidentifiedDelivery:
certificate: |
CiYI5A8SIQUO+IlR4c9wwDNmYkD4RRJAob3q9UG4vwkaV1k9iiBPhAXv5liN85Q8vFwEJawOWxIng6WwXo/hoXNFb/TinsRPlI5XuSfQs23FDUVRVXZggQ== #fake
privateKey: |
iCTXE4i2P1t5Y7Vy7SujSQmFfW1E4= #fake
expiresDays: 365 voiceVerification:
url: http://127.0.0.1 #fake
locales: [ "en-US" ] #fake recaptcha:
secret: 6LfFFFFFFAAAAAAKKKKKKEEEEEEgJ7kX-pSL storageService:
userAuthenticationTokenSharedSecret: 112233445566 #fake backupService:
userAuthenticationTokenSharedSecret: 778899AABBCC #fake transparentDataIndex: # Map<String, String> #server: #use default values in source code
#logging: #use default values in source code
#metrics: #use default values in source code

服务端和客户端代码版本不匹配的问题

最后还有一个坑需要说明:目前(2020-03-16)github上,Signal Server的master分支最新代码和iOS客户端的master分支最新代码并不是匹配的。证据呢?依然是通过日志、调试器这两大利器。

iOS手机端在保存用户的姓名时,会给服务器发一个这样的PUT请求,服务器返回HTTP 400,

/v1/profile/name/MN329VW0j6XOVNH8pgqLOyjmYTdddBp8cBE5B2mPILy4jzAb8PTABCcfep4FfQwPJIRJKQqnhooZjuQrzRSeCeC%2FUzWRqDMN9XFEDIvoCFWrl 

Apache的详细log中会显示是Signal Server返回的HTTP 400。Signal Server用的容器框架是Ketty,指定的logger类是SLF4J,而SLF4J的日志开关配置要看它绑定的具体实现,如果要打开日志开关看日志,又要折腾半天,还是用调试器吧。操起IDEA远程调试,在服务器上放开5005调试端口,以调试监听方式启动Signal Server:

java -Xdebug -Xrunjdwp:transport=dt_socket,address=0.0.0.0:,server=y,suspend=y -jar TextSecureServer-2.92.jar server config/config.yml

在开发机上的IDEA中设置连接服务器的5005端口调试,在Ketty框架的内部代码中转一大圈,最终会发现,/v1/profile/name/这个URI需要的参数在base64解码后的长度必须是72,否则Ketty直接返回400,根本不会调用setProfile()函数。

@Timed
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}")
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @Length(min = 72,max= 72) Optional<String> name) {
account.setProfileName(name.orElse(null));
accountsManager.update(account);
}

但是,iOS客户端的代码显示编码后的长度是108,也就是解码后的长度是81,不匹配。需要用较早一点的iOS客户端代码版本才行。

结语

一坑复一坑,坑坑何其多!正所谓:山穷水尽疑无路,柳暗花明又一坑。

加密通信软件Signal 2.92版本编译安装折腾手记(Ubuntu 18.04)的更多相关文章

  1. 在Ubuntu 18.04上安装Tensorflow

    我们将经历几个阶段,安装cuda-9.0,cudnn和tensorflow cpu以及tensorflow gpu版本.最后我们将用cuda-9.0安装pytorch.在MARVEl电影中黑寡妇的“我 ...

  2. Ubuntu 18.04 安装 python3.7

    Ubuntu 18.04系统内置了Python 3.6和Python 2.7版本,以下是在Ubuntu 18.04系统中安装Python 3.7版本的方法. 1. 执行所有升级# sudo apt u ...

  3. Ubuntu 18.04 Server上安装LAMP

    由于要进行渗透测试,所以这两天就在搭LAMP的环境(过程及其痛苦) 这里分享一些我遇到的问题. 首先介绍一下我的使用环境  VM虚拟机,ubuntu 与主机NAT连接 由于之前一直使用的是kali(默 ...

  4. Ubuntu 18.04上安装 phpMyAdmin

    我们将安装 phpMyAdmin 在 Ubuntu18.04 上配合 Apache 一起工作. 在安装 phpMyAdmin 之前需要已经安装了LAMP栈并提供了web页面. 如果没有安装可以参照 U ...

  5. Win10+WSL2+Ubuntu 18.04(WSL下)+VS Code(Win10下)+TexLive 2019(Ubuntu下)安装和配置

    本人手头电脑是Win10 Home版全新安装的系统,由于不想在新系统盘里面安装TexLive导致固态硬盘不断扩大,所以,考虑安装Ubuntu做为WSL,然后把TexLive安装在Ubuntu,并通过V ...

  6. 在Ubuntu 18.04中安装Wine QQ、微信、TIM

    近日重新安装了Ubuntu 18.04,因此要重新安装一下Wine QQ.微信之类的,完整安装Wine系列软件一直是一个老大难的问题,网上搜集到的博客也比较零散,因此这里特此写篇博客记录一下 0. 这 ...

  7. Linux下指定版本编译安装LAMP

    说明: 操作系统:CentOS 6.5 64位 需求: 编译安装LAMP运行环境 各软件版本如下: MySQL:mysql-5.1.73 Apache:httpd-2.2.31 PHP:php-5.2 ...

  8. 记录:Ubuntu 18.04 安装 tensorflow-gpu 版本

    狠下心来重新装了系统,探索一下 gpu 版本的安装.比较令人可喜的是,跟着前辈们的经验,还是让我给安装成功了.由于我是新装的系统,就像婴儿般纯净,所以进入系统的第一步就是安装 cuda,只要这个不出错 ...

  9. [笔记] Ubuntu 18.04源码编译安装OpenCV 4.0流程

    标准常规安装方法安装的OpenCV版本比较低,想尝鲜使用4.0版本,只好源码安装. 安装环境 OS:Ubuntu 18.04 64 bit 显卡:NVidia GTX 1080 CUDA:10.0 c ...

随机推荐

  1. 解读网易易盾新一代IoT安全编译器Maze

    一.网易易盾为什么会推出安全编译器Maze? 随着5G时代的到来,会有越来越多的物联网设备走进我们的生活,然而物联网的终端本身普遍处于白盒环境中,***者很容易获取相关的信息进行***,这个时候如果不 ...

  2. 在okhttp的callback回调中加Toast出现Cant create handler inside hread that has not called Looper.prepare()...

    2019独角兽企业重金招聘Python工程师标准>>> 分析:callback中回调的response方法中还是在子线程中运行的,所以要调取Toast必须回到主线程中更新ui 解决方 ...

  3. 以内存级速度实现存储?XPoint正是我们的计划

    随着计算能力虚拟化技术的普及,存储机制在速度上远逊于内存这一劣势开始变得愈发凸显. 这一巨大的访问速度鸿沟一直是各项存储技术想要解决的核心难题:纸带.磁带.磁盘驱动器乃至闪存记忆体等等,而如今最新一代 ...

  4. 【STM32 .Net MF开发板学习-05】PC通过Modbus协议远程操控开发板

    从2002年就开始接触Modbus协议,以后陆续在PLC.DOS.Windows..Net Micro Framework等系统中使用了该协议,在我以前写的一篇博文中详细记载了这一段经历,有兴趣的朋友 ...

  5. html入门详细笔记

    Web的基本概念 什么是Web? 中文翻译"网页",它是一些列技术的总称,(包括网站的前台布局.后台程序.美工.数据库开发等),我们称它为网页. Web标准 结构标准(HTML) ...

  6. codeforce 1311 C. Perform the Combo 前缀和

    You want to perform the combo on your opponent in one popular fighting game. The combo is the string ...

  7. H. Subsequences (hard version) dp

    H. Subsequences (hard version) 这个题目好难啊,根本就不知道怎么dp,看了题解,理解了好一会才会的. 首先dp[i][j] 表示前面 i  个字符,形成长度为 j  的不 ...

  8. Collection接口【集合】和Iterator迭代器类

    1.1集合的概述 前面基础学习并使用过集合ArrayList<E>,那么集合究竟是什么呢? 集合:集合是Java中提供的一种容器,可以用来存储多个数据. 那么意思就是说集合是容器,但是容器 ...

  9. Spring Cloud feign GET请求无法用实体传参的解决方法

    代码如下: @FeignClient(name = "eureka-client", fallbackFactory = FallBack.class, decode404 = t ...

  10. CDH删除节点(安全)

    经过验证,最快最安全的删除CDH节点的方式,简单概括如下: 1.选中该机器,并停止该机器上的所有角色 2.去对应的服务中删除该机器中每一个角色 3.通过命令行,关闭该机器的的agent服务,并将该机器 ...