Merge pull request #141 from ssb-ngi-pointer/fix-websocket

Fix websocket
This commit is contained in:
Henry 2021-04-20 18:14:44 +02:00 committed by GitHub
commit a872ddf2c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 485 additions and 171 deletions

View File

@ -368,6 +368,7 @@ func runroomsrv() error {
var httpHandler http.Handler
httpHandler = httpRateLimiter.RateLimit(webHandler)
httpHandler = secureMiddleware.Handler(httpHandler)
httpHandler = roomsrv.Network.WebsockHandler(httpHandler)
// all init was successfull
level.Info(log).Log(

View File

@ -17,3 +17,20 @@ This implementation is compliant with [SSB HTTP Authentication](https://github.c
A summary can be seen in the following chart:
![Chart](./login-chart.png)
# HTTP Hosting
We currently assume a standard HTTPS server in front of go-ssb-room to facilitate TLS termination and certificate management. This should be possible with most modern HTTP servers since it's a pretty standard practice, known as [reverse proxying](https://en.wikipedia.org/wiki/Reverse_proxy).
Two bits of rationale:
1) People usually want to have more than one site on their server. Put differently, we could have [LetsEncrypt](https://letsencrypt.org/) inside the go-ssb-room server but it would have to listen on port :443—blocking the use of other domains on the same IP.
2) Listening on :443 can be pretty annoying (you might need root privileges or similar capabilities).
go-ssb-room needs three headers to function properly, which need to be forwarded by the webserver.
* `X-Forwarded-Host` as which domain name the room is running (enforce strict TLS checking)
* `X-Forwarded-Proto` to ensure that TLS is used (and redirect if necessary)
* `X-Forwarded-For` the remote TCP/IP address of the client accessing the room (used for rate limiting)
[nginx-example.conf](./nginx-example.conf) contains an [nginx](https://nginx.org) config that we use for [hermies.club](https://hermies.club). To get a wildcard TLS certificate you can follow the steps in [this article](https://medium.com/@alitou/getting-a-wildcard-ssl-certificate-using-certbot-and-deploy-on-nginx-15b8ffa34157), which uses the [certbot](https://certbot.eff.org/) utility.

67
docs/nginx-example.conf Normal file
View File

@ -0,0 +1,67 @@
server {
server_name hermies.club;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/hermies.club/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/hermies.club/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
proxy_pass http://localhost:8899;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
proxy_set_header X-Forwarded-Proto $scheme;
# for websocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
# TODO: https://blog.tarq.io/nginx-catch-all-error-pages/
}
# this server uses the (same) wildcard cert as the one above but uses a regular expression on the hostname
# which extracts the first subdomain which holds the alias and forwards that to the prox_pass server
server {
server_name "~^(?<alias>\w+)\.hermies\.club$";
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/hermies.club/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/hermies.club/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location = / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:8899/alias/$alias;
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:8899;
}
# TODO: https://blog.tarq.io/nginx-catch-all-error-pages/
}
server {
if ($host ~ hermies.club$ ) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80 default_server;
listen [::]:80 default_server;
server_name hermies.club;
return 404; # managed by Certbot
}

7
go.mod
View File

@ -14,10 +14,9 @@ require (
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/joefitzgerald/rainbow-reporter v0.1.0 // indirect
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0 // indirect
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
@ -30,7 +29,7 @@ require (
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef // indirect
github.com/volatiletech/sqlboiler/v4 v4.5.0
github.com/volatiletech/strmangle v0.0.1
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414085324-530a7df6ea1c
go.cryptoscope.co/netwrap v0.1.1
go.cryptoscope.co/secretstream v1.2.2
go.mindeco.de v1.11.0
@ -39,7 +38,7 @@ require (
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.5
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

29
go.sum
View File

@ -225,7 +225,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@ -251,7 +250,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
@ -304,6 +302,7 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
@ -319,6 +318,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
@ -382,7 +382,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -485,8 +485,18 @@ go.cryptoscope.co/margaret v0.0.5/go.mod h1:W+Q6lvzHIrF8+Yt3dItHHsx2R9/Xvj/NkJGM
go.cryptoscope.co/margaret v0.0.8/go.mod h1:VbP0bqavqW5osTmdNvgukrqtmqvZC5sXyPSBUW5rzv8=
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4 h1:gLSldWRujtUOfdnpA1XKD71xcCp3Wz1URMnT6xpUPV4=
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4/go.mod h1:3rt+RmZTFZEgfvFxz0ZPDBIWtLJOouWtzV6YbBl6sek=
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59 h1:Gv5pKkvHYJNc12uRZ/jMCsR17G7v6oFLLCrGAUVxhvo=
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.0 h1:Wboamtw7WZPlmEGnGHIPNrScw7HUasXycMmtz+HWXcQ=
go.cryptoscope.co/muxrpc/v2 v2.0.0/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210413143018-bd871b2ec7dd h1:XlPynETXT4tzlzEX8r/V3trDHUDk2ktGol4d3suyC2s=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210413143018-bd871b2ec7dd/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414075132-c1a2adf3d317 h1:in3PvGIazNrexusC+IYXayQ5Ih3CdbWi/oLcArxfnyQ=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414075132-c1a2adf3d317/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414084152-08e3e6acc50d h1:PYOUNvgxKOeJLAF+s+uj31L9xf1xGhqPeHKpORANfTk=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414084152-08e3e6acc50d/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414085324-530a7df6ea1c h1:UQ2PCZ2VDKwvuWXtvyLkCXfdztJPr8tGhTnjsK36JGI=
go.cryptoscope.co/muxrpc/v2 v2.0.1-0.20210414085324-530a7df6ea1c/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/muxrpc/v2 v2.0.1 h1:U1dS1dsFk7/LygH8o2qsWy8dHB5g3YeLtm0lsN7c1CI=
go.cryptoscope.co/muxrpc/v2 v2.0.1/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
go.cryptoscope.co/netwrap v0.1.0/go.mod h1:7zcYswCa4CT+ct54e9uH9+IIbYYETEMHKDNpzl8Ukew=
go.cryptoscope.co/netwrap v0.1.1 h1:JLzzGKEvrUrkKzu3iM0DhpHmt+L/gYqmpcf1lJMUyFs=
go.cryptoscope.co/netwrap v0.1.1/go.mod h1:7zcYswCa4CT+ct54e9uH9+IIbYYETEMHKDNpzl8Ukew=
@ -495,10 +505,6 @@ go.cryptoscope.co/secretstream v1.2.2/go.mod h1:7nRGZ7fTqSgQAnv2Y4m8xQsS3MFxvB7I
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.10.0 h1:H/bhL+dIgZZnUgBEDlKUJBisTszNiHDONeGZtGdiJJ0=
go.mindeco.de v1.10.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.11.0 h1:zrNJFjRr7l+eoRywKxBetB1sgQwnrJ9NA65H/xeusTs=
go.mindeco.de v1.11.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
@ -546,8 +552,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs=
@ -612,7 +616,6 @@ golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6 h1:rbvTkL9AkFts1cgI78+gG6Yu1pwaqX6hjSJAatB78E4=
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -649,8 +652,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=

View File

@ -97,8 +97,12 @@ type Network interface {
GetConnTracker() ConnTracker
// websock hack
HandleHTTP(handler http.Handler)
// WebsockHandler returns a "middleware" like thing that is able to upgrade a
// websocket request to a muxrpc connection and authenticate using shs.
// It calls the next handler if it fails to upgrade the connection to websocket.
// However, it will error on the request and not call the passed handler
// if the websocket upgrade is successfull.
WebsockHandler(next http.Handler) http.Handler
io.Closer
}

View File

@ -43,8 +43,6 @@ type Options struct {
// AfterSecureWrappers are applied afterwards, usefull to debug muxrpc content
AfterSecureWrappers []netwrap.ConnWrapper
WebsocketAddr string
}
type node struct {
@ -110,33 +108,6 @@ func New(opts Options) (Network, error) {
n.log = opts.Logger
// local websocket
wsHandler := websockHandler(n)
httpHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
url := req.URL.String()
if url == "/" {
wsHandler(w, req)
return
}
// n.log.Log("http-url-req", url)
if n.httpHandler != nil {
n.httpHandler.ServeHTTP(w, req)
}
})
if addr := opts.WebsocketAddr; addr != "" {
n.httpLis, err = net.Listen("tcp", addr)
if err != nil {
return nil, err
}
// TODO: move to serve
go func() {
err := http.Serve(n.httpLis, httpHandler)
level.Error(n.log).Log("conn", "ssb-ws :8998 listen exited", "err", err)
}()
}
return n, nil
}

View File

@ -10,128 +10,177 @@ import (
"net/http"
"time"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/websocket"
"go.cryptoscope.co/muxrpc/v2"
)
func websockHandler(n *node) http.HandlerFunc {
// WebsockHandler returns a "middleware" like thing that is able to upgrade a
// websocket request to a muxrpc connection and authenticate using shs.
// It calls the next handler if it fails to upgrade the connection to websocket.
// However, it will error on the request and not call the passed handler
// if the websocket upgrade is successfull.
func (n *node) WebsockHandler(next http.Handler) http.Handler {
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024 * 4,
WriteBufferSize: 1024 * 4,
CheckOrigin: func(_ *http.Request) bool {
return true
},
// 99% of the traffic will be ciphertext which is impossible to distinguish
// from randomness and thus also hard to compress
EnableCompression: false,
// if upgrading fails, just call the next handler and ignore the error
Error: func(w http.ResponseWriter, req *http.Request, _ int, _ error) {
next.ServeHTTP(w, req)
},
}
return func(w http.ResponseWriter, req *http.Request) {
remoteAddr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
if err != nil {
n.log.Log("warning", "failed wrap", "err", err, "remote", remoteAddr)
return
}
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
n.log.Log("warning", "failed wrap", "err", err, "remote", remoteAddr)
return
}
var wc net.Conn
wc = &wrappedConn{
remote: remoteAddr,
local: &net.TCPAddr{
IP: nil,
Port: 8989,
},
wsc: wsConn,
}
var wsh websocketHandelr
wsh.next = next
wsh.upgrader = &upgrader
wsh.muxnetwork = n
// comment out this block to get `noauth` instead of `shs`
// TODO:
// netwrap.WrapAddr(remoteAddr, secretstream.Addr{
// PubKey: n.opts.KeyPair.Id.ID,
// })
cw := n.secretServer.ConnWrapper()
wc, err = cw(wc)
if err != nil {
level.Error(n.log).Log("warning", "failed to crypt", "err", err, "remote", remoteAddr)
wsConn.Close()
return
}
// debugging copy of all muxrpc frames
// can be handy for reversing applications
// wrapped, err := debug.WrapDump("webmux", cryptoConn)
// if err != nil {
// level.Error(n.log).Log("warning", "failed wrap", "err", err, "remote", remoteAddr)
// wsConn.Close()
// return
// }
pkr := muxrpc.NewPacker(wc)
h, err := n.opts.MakeHandler(wc)
if err != nil {
err = fmt.Errorf("unix sock make handler: %w", err)
level.Error(n.log).Log("warn", err)
wsConn.Close()
return
}
edp := muxrpc.Handle(pkr, h,
muxrpc.WithContext(req.Context()),
muxrpc.WithRemoteAddr(wc.RemoteAddr()))
srv := edp.(muxrpc.Server)
// TODO: bundle root and connection context
if err := srv.Serve(); err != nil {
level.Error(n.log).Log("conn", "serve exited", "err", err, "peer", remoteAddr)
}
wsConn.Close()
}
return wsh
}
type wrappedConn struct {
remote net.Addr
local net.Addr
type websocketHandelr struct {
next http.Handler
muxnetwork *node
upgrader *websocket.Upgrader
}
func (wsh websocketHandelr) ServeHTTP(w http.ResponseWriter, req *http.Request) {
remoteAddrStr := req.Header.Get("X-Forwarded-For")
if remoteAddrStr == "" {
remoteAddrStr = req.RemoteAddr
}
remoteAddr, err := net.ResolveTCPAddr("tcp", remoteAddrStr)
if err != nil {
wsh.next.ServeHTTP(w, req)
return
}
wsConn, err := wsh.upgrader.Upgrade(w, req, nil)
if err != nil {
return
}
errLog := level.Error(wsh.muxnetwork.log)
errLog = kitlog.With(errLog, "remote", remoteAddrStr)
var wc net.Conn
wc = NewWebsockConn(wsConn)
cw := wsh.muxnetwork.secretServer.ConnWrapper()
wc, err = cw(wc)
if err != nil {
errLog.Log("warning", "failed to authenticate", "err", err, "remote", remoteAddr)
wsConn.Close()
return
}
// debugging copy of all muxrpc frames
// can be handy for reversing applications
// feed, err := GetFeedRefFromAddr(wc.RemoteAddr())
// if err != nil {
// errLog.Log("warning", "failed to get feed after auth", "err", err, "remote", remoteAddr)
// wsConn.Close()
// return
// }
// dumpPath := filepath.Join("webmux", base64.URLEncoding.EncodeToString(feed.ID[:10]), remoteAddrStr)
// wc, err = debug.WrapDump(dumpPath, wc)
// if err != nil {
// errLog.Log("warning", "failed wrap", "err", err, "remote", remoteAddr)
// wsConn.Close()
// return
// }
pkr := muxrpc.NewPacker(wc)
h, err := wsh.muxnetwork.opts.MakeHandler(wc)
if err != nil {
err = fmt.Errorf("websocket make handler failed: %w", err)
errLog.Log("warn", err)
wsConn.Close()
return
}
edp := muxrpc.Handle(pkr, h,
muxrpc.WithContext(req.Context()),
muxrpc.WithRemoteAddr(wc.RemoteAddr()))
srv := edp.(muxrpc.Server)
if err := srv.Serve(); err != nil {
errLog.Log("conn", "serve exited", "err", err, "peer", remoteAddr)
}
wsConn.Close()
}
// WebsockConn emulates a normal net.Conn from a websocket connection
type WebsockConn struct {
r io.Reader
wsc *websocket.Conn
}
func (conn *wrappedConn) Read(data []byte) (int, error) {
func NewWebsockConn(wsc *websocket.Conn) *WebsockConn {
return &WebsockConn{
wsc: wsc,
}
}
func (conn *WebsockConn) Read(data []byte) (int, error) {
if conn.r == nil {
if err := conn.renewReader(); err != nil {
return -1, err
}
}
n, err := conn.r.Read(data)
if err == io.EOF {
if err := conn.renewReader(); err != nil {
return -1, err
}
return conn.Read(data)
n, err = conn.Read(data)
if err != nil {
err = fmt.Errorf("wsConn: failed to read after renew(): %w", err)
return -1, err
}
return n, nil
}
return n, err
if err != nil {
return -1, fmt.Errorf("wsConn: read failed: %w", err)
}
return n, nil
}
func (wc *wrappedConn) renewReader() error {
mt, r, err := wc.wsc.NextReader()
func (conn *WebsockConn) renewReader() error {
mt, r, err := conn.wsc.NextReader()
if err != nil {
return fmt.Errorf("wsConn: failed to get reader: %w", err)
err = fmt.Errorf("wsConn: failed to get reader: %w", err)
return err
}
if mt != websocket.BinaryMessage {
return fmt.Errorf("wsConn: not binary message: %v", mt)
}
wc.r = r
conn.r = r
return nil
}
func (conn wrappedConn) Write(data []byte) (int, error) {
func (conn *WebsockConn) Write(data []byte) (int, error) {
writeCloser, err := conn.wsc.NextWriter(websocket.BinaryMessage)
if err != nil {
return -1, fmt.Errorf("wsConn: failed to create Reader: %w", err)
@ -141,21 +190,41 @@ func (conn wrappedConn) Write(data []byte) (int, error) {
if err != nil {
return -1, fmt.Errorf("wsConn: failed to copy data: %w", err)
}
return int(n), writeCloser.Close()
}
func (conn wrappedConn) Close() error {
func (conn *WebsockConn) Close() error {
return conn.wsc.Close()
}
func (c wrappedConn) LocalAddr() net.Addr { return c.local }
func (c wrappedConn) RemoteAddr() net.Addr { return c.remote }
func (c wrappedConn) SetDeadline(t time.Time) error {
return nil // c.conn.SetDeadline(t)
func (conn *WebsockConn) LocalAddr() net.Addr { return conn.wsc.LocalAddr() }
func (conn *WebsockConn) RemoteAddr() net.Addr { return conn.wsc.RemoteAddr() }
func (conn *WebsockConn) SetDeadline(t time.Time) error {
rErr := conn.wsc.SetReadDeadline(t)
wErr := conn.wsc.SetWriteDeadline(t)
var err error
if rErr != nil {
err = fmt.Errorf("websock conn: failed to set read deadline: %w", rErr)
}
if wErr != nil {
wErr = fmt.Errorf("websock conn: failed to set read deadline: %w", wErr)
if err != nil {
err = fmt.Errorf("both faild: %w and %s", err, wErr)
} else {
err = wErr
}
}
return err
}
func (c wrappedConn) SetReadDeadline(t time.Time) error {
return nil // c.conn.SetReadDeadline(t)
func (conn *WebsockConn) SetReadDeadline(t time.Time) error {
return conn.wsc.SetReadDeadline(t)
}
func (c wrappedConn) SetWriteDeadline(t time.Time) error {
return nil // c.conn.SetWriteDeadline(t)
func (conn *WebsockConn) SetWriteDeadline(t time.Time) error {
return conn.wsc.SetWriteDeadline(t)
}

View File

@ -5,7 +5,6 @@ package gossip
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
@ -16,10 +15,11 @@ import (
// Ping implements the server side of gossip.ping.
// it's idea is mentioned here https://github.com/ssbc/ssb-gossip/#ping-duplex
// and implemented by https://github.com/dominictarr/pull-ping/
//
func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
type arg struct {
Timeout int
// The only argument is the delay between two pings.
// the Javascript code calls this "timeout", tho.
Delay int `json:"timeout"`
}
var args []arg
@ -33,6 +33,17 @@ func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource,
// timeout = time.Minute * time.Duration(args[0].Timeout/(60*1000))
// }
// return sillyPingPong(ctx, peerSrc, peerSnk)
return actualPingPong(ctx, peerSrc, peerSnk)
}
// actually just read and write whenever...
func sillyPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
var (
sendErr = make(chan error)
receiveErr = make(chan error)
)
go func() {
peerSnk.SetEncoding(muxrpc.TypeJSON)
enc := json.NewEncoder(peerSnk)
@ -40,6 +51,8 @@ func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource,
tick := time.NewTicker(5 * time.Second)
defer tick.Stop()
defer close(sendErr)
for {
select {
case <-ctx.Done():
@ -48,27 +61,40 @@ func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource,
}
var pong = encodedTime.Millisecs(time.Now())
err = enc.Encode(pong)
if err != nil {
if err := enc.Encode(pong); err != nil {
sendErr <- err
return
}
}
}()
for peerSrc.Next(ctx) {
var ping encodedTime.Millisecs
err := peerSrc.Reader(func(rd io.Reader) error {
return json.NewDecoder(rd).Decode(&ping)
})
if err != nil {
return err
go func() {
defer close(receiveErr)
for peerSrc.Next(ctx) {
var ping encodedTime.Millisecs
err := peerSrc.Reader(func(rd io.Reader) error {
return json.NewDecoder(rd).Decode(&ping)
})
if err != nil {
receiveErr <- err
return
}
}
// when := time.Time(ping)
// fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
}
return
}()
return nil
select {
case e := <-sendErr:
return e
case e := <-receiveErr:
return e
case <-ctx.Done():
return nil
}
}
// this is how it should work, i think, but it leads to disconnects...
@ -87,8 +113,8 @@ func actualPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *mu
return err
}
when := time.Time(ping)
fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
//when := time.Time(ping)
//fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
pong := encodedTime.Millisecs(time.Now())
err = enc.Encode(pong)

View File

@ -8,19 +8,22 @@ import (
"fmt"
"io"
refs "go.mindeco.de/ssb-refs"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"go.cryptoscope.co/muxrpc/v2"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
refs "go.mindeco.de/ssb-refs"
)
type connectArg struct {
Portal refs.FeedRef `json:"portal"`
Target refs.FeedRef `json:"target"`
Portal refs.FeedRef `json:"portal"` // the room server
Target refs.FeedRef `json:"target"` // which peer the initiator/caller wants to be tunneld to
}
type connectWithOriginArg struct {
connectArg
Origin refs.FeedRef `json:"origin"` // this should be clear from the shs session already
Origin refs.FeedRef `json:"origin"` // who started the call
}
func (h *Handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
@ -36,6 +39,16 @@ func (h *Handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *mux
}
arg := args[0]
if !arg.Portal.Equal(&h.self) {
return fmt.Errorf("talking to the wrong room")
}
// who made the call
caller, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return err
}
// see if we have and endpoint for the target
edp, has := h.state.Has(arg.Target)
@ -46,20 +59,16 @@ func (h *Handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *mux
// call connect on them
var argWorigin connectWithOriginArg
argWorigin.connectArg = arg
argWorigin.Origin = h.self
argWorigin.Origin = *caller
targetSrc, targetSnk, err := edp.Duplex(ctx, muxrpc.TypeBinary, muxrpc.Method{"tunnel", "connect"}, argWorigin)
if err != nil {
h.state.Remove(arg.Target)
// TODO: the call could fail because of an error with the caller, too.
// if we remove the wrong one, tho others might get confused
// h.state.Remove(caller)
return fmt.Errorf("failed to init connect call with target: %w", err)
}
// pipe data
// pipe data between caller and target
var cpy muxrpcDuplexCopy
cpy.logger = kitlog.With(h.logger, "caller", caller.ShortRef(), "target", arg.Target.ShortRef())
cpy.ctx, cpy.cancel = context.WithCancel(ctx)
go cpy.do(targetSnk, peerSrc)
@ -71,6 +80,8 @@ func (h *Handler) connect(ctx context.Context, req *muxrpc.Request, peerSrc *mux
type muxrpcDuplexCopy struct {
ctx context.Context
cancel context.CancelFunc
logger kitlog.Logger
}
func (mdc muxrpcDuplexCopy) do(w *muxrpc.ByteSink, r *muxrpc.ByteSource) {
@ -80,14 +91,15 @@ func (mdc muxrpcDuplexCopy) do(w *muxrpc.ByteSink, r *muxrpc.ByteSource) {
return err
})
if err != nil {
fmt.Println("read failed:", err)
level.Warn(mdc.logger).Log("event", "read failed", "err", err)
w.CloseWithError(err)
mdc.cancel()
return
}
}
if err := r.Err(); err != nil {
fmt.Println("src errored:", err)
level.Warn(mdc.logger).Log("event", "source errored", "err", err)
// TODO: remove reading side from state?!
w.CloseWithError(err)
mdc.cancel()
}

View File

@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
@ -98,23 +99,28 @@ func (h *Handler) endpoints(ctx context.Context, req *muxrpc.Request, snk *muxrp
}
type updateForwarder struct {
mu sync.Mutex // only one caller to forwarder at a time
snk *muxrpc.ByteSink
enc *json.Encoder
}
func newForwarder(snk *muxrpc.ByteSink) updateForwarder {
func newForwarder(snk *muxrpc.ByteSink) *updateForwarder {
enc := json.NewEncoder(snk)
snk.SetEncoding(muxrpc.TypeJSON)
return updateForwarder{
return &updateForwarder{
snk: snk,
enc: enc,
}
}
func (uf updateForwarder) Update(members []string) error {
func (uf *updateForwarder) Update(members []string) error {
uf.mu.Lock()
defer uf.mu.Unlock()
return uf.enc.Encode(members)
}
func (uf updateForwarder) Close() error {
func (uf *updateForwarder) Close() error {
uf.mu.Lock()
defer uf.mu.Unlock()
return uf.snk.Close()
}

View File

@ -0,0 +1,134 @@
package go_test
import (
"context"
"encoding/base64"
"encoding/json"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"go.cryptoscope.co/muxrpc/v2/debug"
"go.mindeco.de/encodedTime"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
"go.cryptoscope.co/muxrpc/v2"
"go.cryptoscope.co/secretstream"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
)
func TestWebsocketDialing(t *testing.T) {
r := require.New(t)
testPath := filepath.Join("testrun", t.Name())
os.RemoveAll(testPath)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
// create the roomsrv
serverMembers, server := makeNamedTestBot(t, "server", nil)
// open a TCP listener for HTTP
l, err := net.Listen("tcp4", "localhost:0")
r.NoError(err)
var fh = failHandler{t: t}
// serve the websocket handler
handler := server.Network.WebsockHandler(fh)
go http.Serve(l, handler)
// create a fresh keypair for the client
client, err := keys.NewKeyPair(nil)
r.NoError(err)
// add it as a memeber
memberID, err := serverMembers.Add(ctx, client.Feed, roomdb.RoleMember)
r.NoError(err)
t.Log("client member:", memberID)
// construct the websocket http address
var wsURL url.URL
wsURL.Scheme = "ws"
wsURL.Host = l.Addr().String()
wsURL.Path = "/"
// create a websocket connection
conn, resp, err := websocket.DefaultDialer.DialContext(ctx, wsURL.String(), nil)
r.NoError(err)
t.Log(resp.Status)
r.Equal(http.StatusSwitchingProtocols, resp.StatusCode)
// default app key for the secret-handshake connection
ak, err := base64.StdEncoding.DecodeString("1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=")
r.NoError(err)
// create a shs client to authenticate and encrypt the connection
clientSHS, err := secretstream.NewClient(client.Pair, ak)
r.NoError(err)
// the returned wrapper: func(net.Conn) (net.Conn, error)
// returns a new connection that went through shs and does boxstream
connAuther := clientSHS.ConnWrapper(server.Whoami().PubKey())
// turn a websocket conn into a net.Conn
wrappedConn := network.NewWebsockConn(conn)
authedConn, err := connAuther(wrappedConn)
r.NoError(err)
var muxMock muxrpc.FakeHandler
debugConn := debug.Dump(filepath.Join(testPath, "client"), authedConn)
pkr := muxrpc.NewPacker(debugConn)
wsEndpoint := muxrpc.Handle(pkr, &muxMock, muxrpc.WithContext(ctx))
srv := wsEndpoint.(muxrpc.Server)
go func() {
err = srv.Serve()
r.NoError(err)
t.Log("mux server error:", err)
}()
// check we are talking to a room
var yup bool
err = wsEndpoint.Async(ctx, &yup, muxrpc.TypeJSON, muxrpc.Method{"tunnel", "isRoom"})
r.NoError(err)
r.True(yup, "server is not a room?")
// open the gossip.ping channel
src, snk, err := wsEndpoint.Duplex(ctx, muxrpc.TypeJSON, muxrpc.Method{"gossip", "ping"})
r.NoError(err)
pingTS := encodedTime.NewMillisecs(time.Now().Unix())
err = json.NewEncoder(snk).Encode(pingTS)
r.NoError(err)
r.True(src.Next(ctx))
pong, err := src.Bytes()
r.NoError(err)
var pongTS encodedTime.Millisecs
err = json.Unmarshal(pong, &pongTS)
r.NoError(err)
r.False(time.Time(pongTS).IsZero())
}
type failHandler struct {
t *testing.T
}
func (fh failHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
fh.t.Error("next handler called", req.URL.String())
}

View File

@ -60,8 +60,6 @@ func (s *Server) initNetwork() error {
ConnTracker: s.networkConnTracker,
BefreCryptoWrappers: s.preSecureWrappers,
AfterSecureWrappers: s.postSecureWrappers,
WebsocketAddr: s.wsAddr,
}
var err error

View File

@ -32,6 +32,10 @@ const manifest manifestHandler = `
"whoami":"async",
"gossip": {
"ping": "duplex"
},
"room": {
"registerAlias": "async",
"revokeAlias": "async",

View File

@ -2,6 +2,7 @@ package roomstate
import (
"context"
"sort"
"sync"
"time"
@ -73,6 +74,7 @@ func (rsm roomStateMap) AsList() []string {
for m := range rsm {
memberList = append(memberList, m)
}
sort.Strings(memberList)
return memberList
}
@ -108,7 +110,8 @@ func (m *Manager) Remove(who refs.FeedRef) {
m.roomMu.Unlock()
}
// AlreadyAdded returns true if the peer was already added to the room
// AlreadyAdded returns true if the peer was already added to the room.
// if it isn't it will be added.
func (m *Manager) AlreadyAdded(who refs.FeedRef, edp muxrpc.Endpoint) bool {
m.roomMu.Lock()