Skip to content

Commit da13ba9

Browse files
committed
Rework introspection
`xmlrpc.introspection` is now a more general Middleware method.
1 parent a004009 commit da13ba9

File tree

6 files changed

+287
-27
lines changed

6 files changed

+287
-27
lines changed

Sources/MacroXmlRpc/Introspection.swift

Lines changed: 202 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,221 @@
66
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
77
//
88

9-
public extension RouteKeeper {
10-
11-
@inlinable
12-
@discardableResult
13-
func systemListMethods() -> Self {
14-
post { req, res, next in
15-
guard let call = XmlRpc.parseCall(req.body.text ?? ""),
16-
call.methodName == "system.listMethods" else {
17-
return next()
9+
import protocol XmlRpc.XmlRpcValueRepresentable
10+
11+
public extension xmlrpc {
12+
13+
/**
14+
* Provide method reflection information, as described in:
15+
*
16+
* http://xmlrpc-c.sourceforge.net/introspection.html
17+
*
18+
* This middleware needs to be hooked up to the END, after all XML-RPC
19+
* functions have been registered in a route.
20+
*
21+
* Example:
22+
*
23+
* app.route("/RPC2")
24+
* .use(bodyParser.xmlRpcCall())
25+
* .rpc("ping") { _ in "pong" }
26+
* .rpc("add") { ( a: Int, b: Int ) in a + b }
27+
* .use(xmlrpc.introspection())
28+
*
29+
* Specifically hosts implementations for those XML-RPC methods:
30+
* - system.listMethods
31+
* - system.methodExists
32+
* - system.methodSignature
33+
* - system.methodHelp
34+
* - getCapabilities
35+
*
36+
* Note: Careful w/ security implications!
37+
*/
38+
static func introspection() -> Middleware {
39+
return { req, res, next in
40+
guard let call = req.xmlRpcCall else { return next() }
41+
42+
var missingNameResponse : XmlRpc.Response {
43+
req.log.error("XML-RPC introspection call was missing a name!")
44+
return XmlRpc.Response.fault(
45+
.init(code: 400, reason: "Missing method name parameter!"))
46+
}
47+
func unknownMethodResponse(_ name: String) -> XmlRpc.Response {
48+
req.log.warn(
49+
"XML-RPC introspection was asking for an unknown name \(name)!")
50+
return XmlRpc.Response.fault(
51+
.init(code: 404, reason: "Unknown method '\(name)'."))
52+
}
53+
54+
switch call.methodName {
55+
56+
case "system.listMethods":
57+
xmlRpcIntrospectionMethods.forEach { req.addKnownXmlRpcMethod($0) }
58+
return res.send(XmlRpc.Response(req.knownXmlRpcMethodNames).xmlString)
59+
60+
case "system.methodSignature":
61+
guard let methodName = call.parameters.first?.stringValue else {
62+
return res.send(missingNameResponse.xmlString)
63+
}
64+
65+
switch methodName {
66+
case "system.listMethods", "getCapabilities":
67+
req.addSignature([], for: methodName)
68+
case "system.methodSignature", "system.methodHelp",
69+
"system.methodExist":
70+
req.addSignature([ .string ], for: methodName)
71+
default: break
72+
}
73+
74+
if let signatures = req.signatures[methodName] {
75+
return res.send(XmlRpc.Response(signatures).xmlString)
76+
}
77+
else if req.doesMethodExist(methodName) {
78+
// Forbidden by standard, if we say we do introspection, we should.
79+
// :-)
80+
return res.sendStatus(500)
81+
}
82+
else {
83+
return res.send(unknownMethodResponse(methodName).xmlString)
84+
}
85+
86+
case "system.methodHelp":
87+
guard let methodName = call.parameters.first?.stringValue else {
88+
return res.send(missingNameResponse.xmlString)
89+
}
90+
91+
if let help = req.helps[methodName] {
92+
return res.send(XmlRpc.Response(help).xmlString)
93+
}
94+
else if let signatures = req.signatures[methodName],
95+
!signatures.isEmpty
96+
{
97+
let help = generateHelpForMethod(methodName, signatures: signatures)
98+
return res.send(XmlRpc.Response(help).xmlString)
99+
}
100+
else if req.doesMethodExist(methodName) {
101+
let help = "The method '\(methodName)' exists, "
102+
+ "but no documentation is available."
103+
return res.send(XmlRpc.Response(help).xmlString)
104+
}
105+
else {
106+
return res.send(unknownMethodResponse(methodName).xmlString)
107+
}
108+
109+
case "system.methodExist":
110+
guard let methodName = call.parameters.first?.stringValue else {
111+
return res.send(missingNameResponse.xmlString)
112+
}
113+
return res.send(XmlRpc.Response(
114+
req.doesMethodExist(methodName)).xmlString)
115+
116+
case "getCapabilities":
117+
// http://xmlrpc-c.sourceforge.net/doc/libxmlrpc_server.html#system.getCapabilities
118+
// TBD: we could collect more capabilities
119+
return res.send(XmlRpc.Response([
120+
"introspection": [
121+
"specURL" :
122+
"http://xmlrpc-c.sourceforge.net/xmlrpc-c/introspection.html",
123+
"specVersion" : 1
124+
]
125+
]).xmlString)
126+
127+
default:
128+
req.log.warn("unprocessed XML-RPC request after introspection:",
129+
call.methodName)
130+
next()
18131
}
19-
res.send(XmlRpc.Response(req.knownXmlRpcMethodNames).xmlString)
20132
}
21133
}
22134
}
23135

136+
@usableFromInline
137+
let xmlRpcIntrospectionMethods : Set<String> = [
138+
"system.listMethods", "system.methodSignature", "system.methodHelp",
139+
"system.methodExist", "getCapabilities"
140+
]
141+
142+
fileprivate extension IncomingMessage {
143+
144+
func doesMethodExist(_ methodName: String) -> Bool {
145+
let exists = xmlRpcIntrospectionMethods .contains(methodName)
146+
|| self.knownXmlRpcMethodNames.contains(methodName)
147+
return exists
148+
}
149+
}
150+
151+
@usableFromInline
152+
let xmlRpcMethodsRequestKey = "macro.xmlrpc.method-names"
153+
@usableFromInline
154+
let xmlRpcHelpsRequestKey = "macro.xmlrpc.method-helps"
155+
@usableFromInline
156+
let xmlRpcMethodSignaturesRequestKey = "macro.xmlrpc.method-signatures"
157+
24158
extension IncomingMessage {
25159

26-
@usableFromInline
27-
var knownXmlRpcMethodNames : [ String ] {
28-
return (extra["rpc.methods"] as? [ String ]) ?? []
160+
fileprivate var knownXmlRpcMethodNames : Set<String> {
161+
return (extra[xmlRpcMethodsRequestKey] as? Set<String>) ?? []
29162
}
30163

31164
@usableFromInline
32165
func addKnownXmlRpcMethod(_ methodName: String) {
33-
if var methods = extra.removeValue(forKey: "rpc.methods") as? [ String ] {
34-
methods.append(methodName)
35-
extra["rpc.methods"] = methods
166+
if var methods = extra.removeValue(forKey: xmlRpcMethodsRequestKey)
167+
as? Set<String>
168+
{
169+
methods.insert(methodName)
170+
extra[xmlRpcMethodsRequestKey] = methods
36171
}
37172
else {
38-
extra["rpc.methods"] = [ methodName ]
173+
extra[xmlRpcMethodsRequestKey] = Set([ methodName ])
39174
}
40175
}
176+
177+
@usableFromInline
178+
func addHelp(_ help: String, for method: String) {
179+
var helps = extra.removeValue(forKey: xmlRpcHelpsRequestKey)
180+
as? [ String : String ]
181+
?? [:]
182+
helps[method] = help
183+
extra[xmlRpcHelpsRequestKey] = helps
184+
}
185+
186+
@usableFromInline
187+
func addSignature(_ signature: [ XmlRpc.Value.ValueType ],
188+
for method: String)
189+
{
190+
var values = extra.removeValue(forKey: xmlRpcMethodSignaturesRequestKey)
191+
as? [ String : [ [ XmlRpc.Value.ValueType ] ] ]
192+
?? [:]
193+
values[method, default: []].append(signature)
194+
extra[xmlRpcMethodSignaturesRequestKey] = values
195+
}
196+
197+
fileprivate var helps : [ String : String ] {
198+
return (extra[xmlRpcHelpsRequestKey] as? [ String : String ]) ?? [:]
199+
}
200+
fileprivate var signatures : [ String : [ [ XmlRpc.Value.ValueType ] ] ] {
201+
return (extra[xmlRpcMethodSignaturesRequestKey]
202+
as? [ String : [ [ XmlRpc.Value.ValueType ] ] ])
203+
?? [:]
204+
}
205+
}
206+
207+
fileprivate
208+
func generateHelpForMethod(_ methodName: String,
209+
signatures: [ [ XmlRpc.Value.ValueType ] ])
210+
-> String
211+
{
212+
if signatures.isEmpty {
213+
return
214+
"The method '\(methodName)' exists, but no documentation is available."
215+
}
216+
217+
var ms =
218+
"The method '\(methodName)' can be called with the following signatures:"
219+
ms += "\n\n"
220+
for signature in signatures {
221+
ms += " \(methodName)("
222+
ms += signature.map { $0.xmlRpcValue.stringValue }.joined(separator: ", ")
223+
ms += ")"
224+
}
225+
return ms
41226
}

Sources/MacroXmlRpc/Middleware.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public extension xmlrpc {
5555
do {
5656
let value = try execute(call)
5757

58-
req.log.log("executed request:", call.methodName)
5958
return res.send(XmlRpc.Response.value(value.xmlRpcValue).xmlString)
6059
}
6160
catch let error as XmlRpc.Fault {

Sources/MacroXmlRpc/RouteKeeper.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ public extension RouteKeeper {
2626

2727
@inlinable
2828
@discardableResult
29-
func rpc<A1>(_ methodName: String,
30-
execute: @escaping ( A1 )
31-
throws -> XmlRpcValueRepresentable)
29+
func rpc<A1, R>(_ methodName: String, execute: @escaping ( A1 ) throws -> R)
3230
-> Self
33-
where A1: XmlRpcValueRepresentable
31+
where A1 : XmlRpcValueRepresentable,
32+
R : XmlRpcValueRepresentable
3433
{
3534
rpc(methodName) { call in
3635
guard call.parameters.count == 1,
@@ -42,12 +41,12 @@ public extension RouteKeeper {
4241

4342
@inlinable
4443
@discardableResult
45-
func rpc<A1, A2>(_ methodName: String,
46-
execute: @escaping ( A1, A2 )
47-
throws -> XmlRpcValueRepresentable)
44+
func rpc<A1, A2, R>(_ methodName: String,
45+
execute: @escaping ( A1, A2 ) throws -> R)
4846
-> Self
49-
where A1: XmlRpcValueRepresentable,
50-
A2: XmlRpcValueRepresentable
47+
where A1 : XmlRpcValueRepresentable,
48+
A2 : XmlRpcValueRepresentable,
49+
R : XmlRpcValueRepresentable
5150
{
5251
rpc(methodName) { call in
5352
guard call.parameters.count == 2,

Sources/MacroXmlRpc/Server.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//
2+
// Server.swift
3+
// MacroXmlRpc
4+
//
5+
// Created by Helge Hess.
6+
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
7+
//
8+
9+
// TODO: Try to mirror the JS server in http://xmlrpc.com
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// ValueType.swift
3+
// MacroXmlRpc
4+
//
5+
// Created by Helge Hess.
6+
// Copyright © 2020 ZeeZide GmbH. All rights reserved.
7+
//
8+
9+
import protocol XmlRpc.XmlRpcValueRepresentable
10+
11+
public extension XmlRpc.Value {
12+
13+
/**
14+
* The various possible XML-RPC value types.
15+
*/
16+
@frozen
17+
enum ValueType: Hashable {
18+
case null
19+
case string, bool, int, double, dateTime, data
20+
case array, dictionary
21+
}
22+
23+
@inlinable
24+
var xmlRpcValueType: ValueType {
25+
switch self {
26+
case .null : return .null
27+
case .string : return .string
28+
case .bool : return .bool
29+
case .int : return .int
30+
case .double : return .double
31+
case .dateTime : return .dateTime
32+
case .data : return .data
33+
case .array : return .array
34+
case .dictionary : return .dictionary
35+
}
36+
}
37+
}
38+
39+
extension XmlRpc.Value.ValueType: XmlRpcValueRepresentable {
40+
41+
public init?(xmlRpcValue: XmlRpc.Value) {
42+
switch xmlRpcValue.stringValue {
43+
case "i4", "int" : self = .int
44+
case "boolean" : self = .bool
45+
case "string" : self = .string
46+
case "double" : self = .double
47+
case "base64" : self = .data
48+
case "dateTime.iso8601" : self = .dateTime
49+
case "struct" : self = .dictionary
50+
case "array" : self = .array
51+
case "null" : self = .null
52+
default: return nil
53+
}
54+
}
55+
public var xmlRpcValue : XmlRpc.Value {
56+
switch self {
57+
case .null : return "null"
58+
case .string : return "string"
59+
case .bool : return "boolean"
60+
case .int : return "int"
61+
case .double : return "double"
62+
case .dateTime : return "dateTime.iso8601"
63+
case .data : return "base64"
64+
case .array : return "array"
65+
case .dictionary : return "struct"
66+
}
67+
}
68+
}

TODO.md

Whitespace-only changes.

0 commit comments

Comments
 (0)