diff --git a/internal/mtprotoedge/server.go b/internal/mtprotoedge/server.go index b2dfe148..4c7bb906 100644 --- a/internal/mtprotoedge/server.go +++ b/internal/mtprotoedge/server.go @@ -517,6 +517,12 @@ type Server struct { rpcResults *rpcExecutionLedger rpcRewrap *rpcRewrapRegistry + // pubKeyPEM is the server's RSA public key, precomputed once at + // construction and served over the same-port HTTP side (see + // server_info_http.go) so a client's "Add Server" flow can self-configure + // from a host:port instead of a manual openssl + copy-paste. + pubKeyPEM []byte + // onFrame 是测试钩子:收到一帧时回调其字节数;生产为 nil。 onFrame func(n int) } @@ -554,6 +560,7 @@ func New(opts Options) *Server { dc: opts.DC, strictDC: opts.StrictDC, key: exchange.PrivateKey{RSA: opts.RSAKey}, + pubKeyPEM: rsaPublicKeyPEM(opts.RSAKey), authKeys: opts.AuthKeys, conns: conns, rpc: opts.legacyRPC, @@ -707,7 +714,11 @@ func (s *Server) serveMixed(ctx context.Context, ln net.Listener) error { wsLn := newTransportPacketMessageListener(wsRawLn) httpServer := &http.Server{ - Handler: websocketRouteHandler(wsHandler, s.websocketOrigins), + Handler: serverInfoHTTPHandler( + websocketRouteHandler(wsHandler, s.websocketOrigins), + s.dc, + s.pubKeyPEM, + ), ReadHeaderTimeout: minDuration(10*time.Second, s.handshakeTimeout), BaseContext: func(net.Listener) context.Context { return ctx diff --git a/internal/mtprotoedge/server_info_http.go b/internal/mtprotoedge/server_info_http.go new file mode 100644 index 00000000..d6b389e9 --- /dev/null +++ b/internal/mtprotoedge/server_info_http.go @@ -0,0 +1,74 @@ +package mtprotoedge + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "net/http" +) + +// ServerInfoPath is the well-known same-port HTTP path a client can GET to +// self-configure an "Add Server" entry from just a host:port -- no more +// manual `openssl rsa -RSAPublicKey_out` + copy-paste of the PEM block. +// +// Served over the same samePortMux HTTP side that already carries WebSocket +// upgrades (see same_port_mux.go): a plain GET here never looks like an +// obfuscated2 init (isHTTPHeaderPrefix's "legal init headers never start +// with GET/POST/HEAD/OPTI" invariant), so this adds no new port and no new +// risk to the raw-TCP detection path. Only reachable when serveMixed is +// active, i.e. TELESRV_WEBSOCKET_ENABLE=true (the default). +const ServerInfoPath = "/owpengram/server-info" + +// ServerInfoResponse is ServerInfoPath's JSON body. RSAPublicKeyPEM is the +// PKCS#1 "RSA PUBLIC KEY" PEM block -- the same format +// `openssl rsa -RSAPublicKey_out` produces, and what the client's "Add +// Server" RSA key field already expects verbatim. +type ServerInfoResponse struct { + DCID int `json:"dc_id"` + RSAPublicKeyPEM string `json:"rsa_public_key_pem"` +} + +// rsaPublicKeyPEM renders key's public half as a PKCS#1 PEM block, matching +// `openssl rsa -in server_rsa.pem -RSAPublicKey_out` byte-for-byte (that +// flag selects PKCS#1 encoding, not the x509/PKIX default `-pubout` would +// produce -- a real format difference, not just a header/footer string). +func rsaPublicKeyPEM(key *rsa.PrivateKey) []byte { + if key == nil { + return nil + } + der := x509.MarshalPKCS1PublicKey(&key.PublicKey) + return pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: der}) +} + +// serverInfoHTTPHandler serves ServerInfoResponse at ServerInfoPath and +// delegates every other path to next (the existing WebSocket route +// handler). pubKeyPEM is nil when the server has no RSA key configured +// (shouldn't happen in production -- handshakes would already be broken -- +// but a client asking anyway gets 503, not a panic or an empty key). +func serverInfoHTTPHandler(next http.Handler, dc int, pubKeyPEM []byte) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != ServerInfoPath { + next.ServeHTTP(w, r) + return + } + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if len(pubKeyPEM) == 0 { + http.Error(w, "server key not configured", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + if r.Method == http.MethodHead { + return + } + _ = json.NewEncoder(w).Encode(ServerInfoResponse{ + DCID: dc, + RSAPublicKeyPEM: string(pubKeyPEM), + }) + }) +}