250 lines
6.8 KiB
Go
250 lines
6.8 KiB
Go
// mautrix-signal - A Matrix-signal puppeting bridge.
|
|
// Copyright (C) 2023 Scott Weber
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
|
)
|
|
|
|
const proxyUrlStr = "" // Set this to proxy requests
|
|
const caCertPath = "" // Set this to trust a self-signed cert (ie. for mitmproxy)
|
|
|
|
var UserAgent = "signalmeow/0.1.0 libsignal/" + libsignalgo.Version + " go/" + strings.TrimPrefix(runtime.Version(), "go")
|
|
var SignalAgent = "MAU"
|
|
|
|
const (
|
|
APIHostname = "chat.signal.org"
|
|
StorageHostname = "storage.signal.org"
|
|
CDN1Hostname = "cdn.signal.org"
|
|
CDN2Hostname = "cdn2.signal.org"
|
|
CDN3Hostname = "cdn3.signal.org"
|
|
)
|
|
|
|
var CDNHosts = []string{
|
|
CDN1Hostname,
|
|
CDN1Hostname,
|
|
CDN2Hostname,
|
|
CDN3Hostname,
|
|
}
|
|
|
|
//go:embed signal-root.crt.der
|
|
var signalRootCertBytes []byte
|
|
var signalTransport = &http.Transport{
|
|
ForceAttemptHTTP2: true,
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: x509.NewCertPool(),
|
|
},
|
|
}
|
|
var SignalHTTPClient = &http.Client{
|
|
Transport: signalTransport,
|
|
}
|
|
|
|
func init() {
|
|
cert, err := x509.ParseCertificate(signalRootCertBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signalTransport.TLSClientConfig.RootCAs.AddCert(cert)
|
|
|
|
if proxyUrlStr != "" {
|
|
proxyURL, err := url.Parse(proxyUrlStr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signalTransport.Proxy = http.ProxyURL(proxyURL)
|
|
}
|
|
if caCertPath != "" {
|
|
caCert, err := os.ReadFile(caCertPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signalTransport.TLSClientConfig.RootCAs.AppendCertsFromPEM(caCert)
|
|
}
|
|
}
|
|
|
|
type ContentType string
|
|
|
|
const (
|
|
ContentTypeJSON ContentType = "application/json"
|
|
ContentTypeProtobuf ContentType = "application/x-protobuf"
|
|
ContentTypeOctetStream ContentType = "application/octet-stream"
|
|
)
|
|
|
|
type HTTPReqOpt struct {
|
|
Body []byte
|
|
Username *string
|
|
Password *string
|
|
ContentType ContentType
|
|
Host string
|
|
Headers map[string]string
|
|
OverrideURL string // Override the full URL, if set ignores path and Host
|
|
}
|
|
|
|
var httpReqCounter = 0
|
|
|
|
func SendHTTPRequest(ctx context.Context, method string, path string, opt *HTTPReqOpt) (*http.Response, error) {
|
|
// Set defaults
|
|
if opt == nil {
|
|
opt = &HTTPReqOpt{}
|
|
}
|
|
if opt.Host == "" {
|
|
opt.Host = APIHostname
|
|
}
|
|
if len(path) > 0 && path[0] != '/' {
|
|
path = "/" + path
|
|
}
|
|
urlStr := "https://" + opt.Host + path
|
|
if opt.OverrideURL != "" {
|
|
urlStr = opt.OverrideURL
|
|
}
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str("action", "send HTTP request").
|
|
Str("method", method).
|
|
Str("url", urlStr).
|
|
Logger()
|
|
ctx = log.WithContext(ctx)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, urlStr, bytes.NewBuffer(opt.Body))
|
|
if err != nil {
|
|
log.Err(err).Msg("Error creating request")
|
|
return nil, err
|
|
}
|
|
if opt.Headers != nil {
|
|
for k, v := range opt.Headers {
|
|
req.Header.Add(k, v)
|
|
}
|
|
}
|
|
if opt.ContentType != "" {
|
|
req.Header.Set("Content-Type", string(opt.ContentType))
|
|
} else {
|
|
req.Header.Set("Content-Type", string(ContentTypeJSON))
|
|
}
|
|
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(opt.Body)))
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
req.Header.Set("X-Signal-Agent", SignalAgent)
|
|
if opt.Username != nil && opt.Password != nil {
|
|
req.SetBasicAuth(*opt.Username, *opt.Password)
|
|
}
|
|
|
|
httpReqCounter++
|
|
log = log.With().Int("request_number", httpReqCounter).Logger()
|
|
log.Trace().Msg("Sending HTTP request")
|
|
resp, err := SignalHTTPClient.Do(req)
|
|
if err != nil {
|
|
log.Err(err).Msg("Error sending request")
|
|
return nil, err
|
|
}
|
|
log.Debug().Int("status_code", resp.StatusCode).Msg("received HTTP response")
|
|
return resp, nil
|
|
}
|
|
|
|
// DecodeHTTPResponseBody checks status code, reads an http.Response's Body and decodes it into the provided interface.
|
|
func DecodeHTTPResponseBody(ctx context.Context, out any, resp *http.Response) error {
|
|
defer resp.Body.Close()
|
|
|
|
// Check if status code indicates success
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
// Read the whole body and log it
|
|
body, _ := io.ReadAll(resp.Body)
|
|
zerolog.Ctx(ctx).Debug().
|
|
Str("body", string(body)).
|
|
Int("status_code", resp.StatusCode).
|
|
Msg("unexpected status code")
|
|
return fmt.Errorf("Unexpected status code: %d %s", resp.StatusCode, resp.Status)
|
|
}
|
|
|
|
decoder := json.NewDecoder(resp.Body)
|
|
if err := decoder.Decode(&out); err != nil {
|
|
return fmt.Errorf("JSON decoding failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetAttachment(ctx context.Context, path string, cdnNumber uint32, opt *HTTPReqOpt) (*http.Response, error) {
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str("action", "get_attachment").
|
|
Str("path", path).
|
|
Uint32("cdn_number", cdnNumber).
|
|
Logger()
|
|
if opt == nil {
|
|
opt = &HTTPReqOpt{}
|
|
}
|
|
if opt.Host == "" {
|
|
if int(cdnNumber) > len(CDNHosts) {
|
|
log.Warn().Msg("Invalid CDN index")
|
|
opt.Host = CDN1Hostname
|
|
} else {
|
|
opt.Host = CDNHosts[cdnNumber]
|
|
}
|
|
if cdnNumber == 0 {
|
|
// This is basically a fallback if cdnNumber is not set
|
|
// but it also seems to be the right host if cdnNumber == 0
|
|
opt.Host = CDNHosts[0]
|
|
} else if cdnNumber > 0 && int(cdnNumber) <= len(CDNHosts) {
|
|
// Pull CDN hosts from array (cdnNumber is 1-indexed, but we have a placeholder host at index 0)
|
|
// (the 1-indexed is just an assumption, other clients seem to only explicitly handle cdnNumber == 0 and 2)
|
|
opt.Host = CDNHosts[cdnNumber]
|
|
} else {
|
|
opt.Host = CDNHosts[0]
|
|
log.Warn().Msg("Invalid CDN index")
|
|
}
|
|
}
|
|
log.Debug().Str("host", opt.Host).Msg("getting attachment")
|
|
urlStr := "https://" + opt.Host + path
|
|
req, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//const SERVICE_REFLECTOR_HOST = "europe-west1-signal-cdn-reflector.cloudfunctions.net"
|
|
//req.Header.Add("Host", SERVICE_REFLECTOR_HOST)
|
|
req.Header.Add("Content-Type", "application/octet-stream")
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
|
|
httpReqCounter++
|
|
log = log.With().
|
|
Int("request_number", httpReqCounter).
|
|
Str("url", urlStr).
|
|
Logger()
|
|
|
|
log.Debug().Msg("Sending Attachment HTTP request")
|
|
resp, err := SignalHTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Debug().Msg("Received Attachment HTTP response")
|
|
|
|
return resp, err
|
|
}
|