2020-10-06 08:51:32 -05:00
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
2023-05-04 07:11:23 -05:00
"net/http"
2020-10-06 08:51:32 -05:00
"net/url"
"os"
2023-05-04 07:11:23 -05:00
"strings"
"time"
2020-10-06 08:51:32 -05:00
2023-05-04 07:11:23 -05:00
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/gomatrix"
2020-10-06 08:51:32 -05:00
"github.com/matrix-org/gomatrixserverlib"
2023-04-06 03:55:01 -05:00
"github.com/matrix-org/gomatrixserverlib/fclient"
2023-04-19 09:50:33 -05:00
"github.com/matrix-org/gomatrixserverlib/spec"
2020-10-06 08:51:32 -05:00
)
2023-05-04 07:11:23 -05:00
var (
flagSkipVerify bool // -k, --insecure Allow insecure server connections
flagMethod string // -X, --request <method> Specify request method to use
flagData string // -d, --data <data> HTTP POST data
2020-10-06 08:51:32 -05:00
2023-05-04 07:11:23 -05:00
flagMatrixKey string
flagOrigin string
2023-05-04 10:55:34 -05:00
flagPort int
2023-05-04 07:11:23 -05:00
)
2020-10-06 08:51:32 -05:00
2023-05-04 07:11:23 -05:00
func init ( ) {
flag . BoolVar ( & flagSkipVerify , "insecure" , false , "Allow insecure server connections" )
flag . BoolVar ( & flagSkipVerify , "k" , false , "Allow insecure server connections" )
flag . StringVar ( & flagMethod , "X" , "GET" , "Specify HTTP request method to use" )
flag . StringVar ( & flagMethod , "request" , "GET" , "Specify HTTP request method to use" )
flag . StringVar ( & flagData , "d" , "" , "HTTP JSON body data. If you start the data with the letter @, the rest should be a filename." )
flag . StringVar ( & flagData , "data" , "" , "HTTP JSON body data. If you start the data with the letter @, the rest should be a filename." )
flag . StringVar ( & flagMatrixKey , "M" , "matrix_key.pem" , "The private key to use when signing the request" )
flag . StringVar ( & flagMatrixKey , "key" , "matrix_key.pem" , "The private key to use when signing the request" )
2020-10-06 08:51:32 -05:00
2023-05-04 07:11:23 -05:00
flag . StringVar ( & flagOrigin , "O" , "" , "The server name that the request should originate from. The remote server will use this to request server keys. There MUST be a TLS listener at the .well-known address for this server name, i.e it needs to be pointing to a real homeserver. If blank, furl will self-host this on a random high numbered port, but only if the target is localhost. Use $PORT in request URLs/bodies to substitute the port number in." )
flag . StringVar ( & flagOrigin , "origin" , "" , "The server name that the request should originate from. The remote server will use this to request server keys. There MUST be a TLS listener at the .well-known address for this server name, i.e it needs to be pointing to a real homeserver. If blank, furl will self-host this on a random high numbered port, but only if the target is localhost. Use $PORT in request URLs/bodies to substitute the port number in." )
2023-05-04 10:55:34 -05:00
flag . IntVar ( & flagPort , "p" , 0 , "Port to self-host on. If set, always self-hosts. Required because sometimes requests need the same origin." )
2023-05-04 07:11:23 -05:00
}
type args struct {
2023-05-04 10:55:34 -05:00
SkipVerify bool
Method string
Data [ ] byte
MatrixKey ed25519 . PrivateKey
MatrixKeyID gomatrixserverlib . KeyID
Origin spec . ServerName
SelfHostKey bool
SelfHostPort int
TargetURL * url . URL
2023-05-04 07:11:23 -05:00
}
func processArgs ( ) ( * args , error ) {
if len ( flag . Arg ( 0 ) ) == 0 {
return nil , fmt . Errorf ( "furl [-k] [-X GET|PUT|POST|DELETE] [-d @filename|{\"inline\":\"json\"}] [-M matrix_key.pem] [-O localhost] https://federation-server.url/_matrix/.../" )
}
targetURL , err := url . Parse ( flag . Arg ( 0 ) )
2020-10-06 08:51:32 -05:00
if err != nil {
2023-05-04 07:11:23 -05:00
return nil , fmt . Errorf ( "invalid url: %s" , err )
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
// load .pem file
data , err := os . ReadFile ( flagMatrixKey )
if err != nil {
return nil , err
}
2020-10-06 08:51:32 -05:00
keyBlock , _ := pem . Decode ( data )
if keyBlock == nil {
2023-05-04 07:11:23 -05:00
return nil , fmt . Errorf ( "invalid pem file: %s" , flagMatrixKey )
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
if keyBlock . Type != "MATRIX PRIVATE KEY" {
return nil , fmt . Errorf ( "pem file bad block type, want MATRIX PRIVATE KEY got %s" , keyBlock . Type )
}
_ , privateKey , err := ed25519 . GenerateKey ( bytes . NewReader ( keyBlock . Bytes ) )
if err != nil {
return nil , err
}
var a args
a . MatrixKey = privateKey
a . MatrixKeyID = gomatrixserverlib . KeyID ( keyBlock . Headers [ "Key-ID" ] )
a . SkipVerify = flagSkipVerify
a . Method = strings . ToUpper ( flagMethod )
a . Origin = spec . ServerName ( flagOrigin )
2023-05-04 10:55:34 -05:00
a . SelfHostPort = flagPort
2023-05-04 07:11:23 -05:00
a . TargetURL = targetURL
2023-05-04 10:55:34 -05:00
a . SelfHostKey = a . SelfHostPort != 0 || ( a . Origin == "" && a . TargetURL . Hostname ( ) == "localhost" )
2023-05-04 07:11:23 -05:00
// load data
isFile := strings . HasPrefix ( flagData , "@" )
if isFile {
a . Data , err = os . ReadFile ( flagData [ 1 : ] )
2020-10-06 08:51:32 -05:00
if err != nil {
2023-05-04 07:11:23 -05:00
return nil , fmt . Errorf ( "failed to read file '%s': %s" , flagData [ 1 : ] , err )
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
} else if len ( flagData ) > 0 {
a . Data = [ ] byte ( flagData )
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
return & a , nil
}
2020-10-06 08:51:32 -05:00
2023-05-04 07:11:23 -05:00
func main ( ) {
flag . Parse ( )
a , err := processArgs ( )
2020-10-06 08:51:32 -05:00
if err != nil {
2023-05-04 07:11:23 -05:00
fmt . Println ( err . Error ( ) )
flag . PrintDefaults ( )
os . Exit ( 1 )
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
if a . SelfHostKey {
fmt . Printf ( "Self-hosting key..." )
2023-05-04 10:55:34 -05:00
apiURL , cancel := test . ListenAndServe ( tt { } , http . DefaultServeMux , true , a . SelfHostPort )
2023-05-04 07:11:23 -05:00
defer cancel ( )
parsedURL , _ := url . Parse ( apiURL )
a . Origin = spec . ServerName ( parsedURL . Host )
fmt . Printf ( " OK on %s\n" , a . Origin )
// handle the request when it comes in
pubKey := a . MatrixKey . Public ( ) . ( ed25519 . PublicKey )
serverKey := gomatrixserverlib . ServerKeyFields {
ServerName : a . Origin ,
ValidUntilTS : spec . AsTimestamp ( time . Now ( ) . Add ( 2 * time . Minute ) ) ,
VerifyKeys : map [ gomatrixserverlib . KeyID ] gomatrixserverlib . VerifyKey {
a . MatrixKeyID : {
Key : spec . Base64Bytes ( pubKey ) ,
} ,
} ,
2020-10-06 08:51:32 -05:00
}
2023-05-04 11:48:03 -05:00
var serverKeyBytes [ ] byte
serverKeyBytes , err = json . Marshal ( serverKey )
2023-05-04 07:11:23 -05:00
if err != nil {
panic ( err )
}
2023-05-05 04:00:10 -05:00
var signedBytes [ ] byte
signedBytes , err = gomatrixserverlib . SignJSON ( string ( a . Origin ) , a . MatrixKeyID , a . MatrixKey , serverKeyBytes )
2023-05-04 07:11:23 -05:00
if err != nil {
panic ( err )
}
resp := map [ string ] interface { } {
"server_keys" : [ ] json . RawMessage { signedBytes } ,
}
respBytes , err := json . Marshal ( resp )
if err != nil {
panic ( err )
}
fmt . Printf ( "Will return %s\n" , string ( respBytes ) )
http . HandleFunc ( "/_matrix/key/v2/query" , func ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "POST" {
w . WriteHeader ( http . StatusMethodNotAllowed )
return
}
w . WriteHeader ( http . StatusOK )
w . Write ( respBytes )
} )
// replace anything with $PORT
port := parsedURL . Port ( )
a . TargetURL , err = url . Parse ( strings . ReplaceAll ( a . TargetURL . String ( ) , "$PORT" , port ) )
if err != nil {
2020-10-06 08:51:32 -05:00
panic ( err )
}
2023-05-04 10:55:34 -05:00
if a . Data != nil {
data := string ( a . Data )
data = strings . ReplaceAll ( data , "$PORT" , port )
a . Data = [ ] byte ( data )
}
2020-10-06 08:51:32 -05:00
}
2023-05-04 07:11:23 -05:00
client := fclient . NewFederationClient (
[ ] * fclient . SigningIdentity {
{
ServerName : a . Origin ,
KeyID : a . MatrixKeyID ,
PrivateKey : a . MatrixKey ,
} ,
} , fclient . WithSkipVerify ( a . SkipVerify ) ,
2020-10-06 08:51:32 -05:00
)
2023-05-04 07:11:23 -05:00
req := fclient . NewFederationRequest (
a . Method ,
a . Origin ,
spec . ServerName ( a . TargetURL . Host ) ,
a . TargetURL . RequestURI ( ) ,
)
if a . Data != nil {
var jsonData interface { }
2023-05-04 11:48:03 -05:00
if err = json . Unmarshal ( a . Data , & jsonData ) ; err != nil {
2023-05-04 07:11:23 -05:00
fmt . Printf ( "Supplied data is not valid json: %s\n" , err )
os . Exit ( 1 )
}
if err = req . SetContent ( jsonData ) ; err != nil {
panic ( err ) // should be impossible as we just checked it was valid JSON
2020-10-06 08:51:32 -05:00
}
}
2023-05-04 07:11:23 -05:00
if err = req . Sign ( a . Origin , a . MatrixKeyID , a . MatrixKey ) ; err != nil {
2020-10-06 08:51:32 -05:00
panic ( err )
}
httpReq , err := req . HTTPRequest ( )
if err != nil {
panic ( err )
}
var res interface { }
err = client . DoRequestAndParseResponse (
context . TODO ( ) ,
httpReq ,
& res ,
)
if err != nil {
2023-05-04 07:11:23 -05:00
mxerr , ok := err . ( gomatrix . HTTPError )
if ok {
fmt . Printf ( "Server returned HTTP %d\n" , mxerr . Code )
fmt . Println ( mxerr . Message )
fmt . Println ( mxerr . WrappedError )
} else {
panic ( err )
}
os . Exit ( 1 )
2020-10-06 08:51:32 -05:00
}
j , err := json . MarshalIndent ( res , "" , " " )
if err != nil {
panic ( err )
}
fmt . Println ( string ( j ) )
}
2023-05-04 07:11:23 -05:00
type tt struct { }
func ( t tt ) Logf ( format string , args ... any ) {
fmt . Printf ( format + "\n" , args ... )
}
func ( t tt ) Errorf ( format string , args ... any ) {
fmt . Printf ( format + "\n" , args ... )
}
func ( t tt ) Fatalf ( format string , args ... any ) {
fmt . Printf ( format + "\n" , args ... )
os . Exit ( 2 )
}
func ( t tt ) TempDir ( ) string {
return os . TempDir ( )
}