-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
IppClient.kt
191 lines (171 loc) · 7.42 KB
/
IppClient.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package de.gmuth.ipp.client
/**
* Copyright (c) 2020-2024 Gerhard Muth
*/
import de.gmuth.ipp.client.IppOperationException.ClientErrorNotFoundException
import de.gmuth.ipp.core.IppOperation
import de.gmuth.ipp.core.IppRequest
import de.gmuth.ipp.core.IppResponse
import de.gmuth.ipp.core.IppStatus.ClientErrorBadRequest
import de.gmuth.ipp.core.IppStatus.ClientErrorNotFound
import de.gmuth.ipp.core.IppTag.Unsupported
import de.gmuth.ipp.iana.IppRegistrationsSection2
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URI
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level.SEVERE
import java.util.logging.Logger
import java.util.logging.Logger.getLogger
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
typealias IppResponseInterceptor = (request: IppRequest, response: IppResponse) -> Unit
open class IppClient(val config: IppConfig = IppConfig()) : IppExchange {
protected val logger: Logger = getLogger(javaClass.name)
var responseInterceptor: IppResponseInterceptor? = null
var saveMessages: Boolean = false
var saveMessagesDirectory = File("ipp-messages")
var onExceptionSaveMessages: Boolean = false
var throwWhenNotSuccessful: Boolean = true
var disconnectAfterHttpPost: Boolean = false
fun basicAuth(user: String, password: String) {
config.userName = user
config.password = password
}
companion object {
const val APPLICATION_IPP = "application/ipp"
}
//-----------------
// Build IppRequest
//-----------------
private val requestCounter = AtomicInteger(1)
fun ippRequest(
operation: IppOperation,
printerUri: URI? = null,
requestedAttributes: Collection<String>? = null,
userName: String? = config.userName
) = IppRequest(
operation,
printerUri,
requestedAttributes,
userName,
config.ippVersion,
requestCounter.getAndIncrement(),
config.charset,
config.naturalLanguage
)
//------------------------------------
// Exchange IppRequest for IppResponse
//------------------------------------
override fun exchange(request: IppRequest) = with(request) {
logger.finer { "Send '$operation' request to $printerOrJobUri" }
httpPost(toHttpUri(printerOrJobUri), request).also {
logger.fine { "Req #${request.requestId} @${printerOrJobUri.host}: $request => $it" }
if (saveMessages) {
fun file(suffix: String) =
File(saveMessagesDirectory, "%03d-%s.%s".format(requestId, operation, suffix))
saveBytes(file("req"))
saveText(file("req.txt"))
it.saveBytes(file("res"))
it.saveText(file("res.txt"))
}
responseInterceptor?.invoke(request, it)
validateIppResponse(request, it)
}
}
//----------------------------------------------
// HTTP post IPP request and decode IPP response
//----------------------------------------------
open fun httpPost(httpUri: URI, request: IppRequest): IppResponse {
with(httpUri.toURL().openConnection() as HttpURLConnection) {
if (this is HttpsURLConnection && config.sslContext != null) {
sslSocketFactory = config.sslContext!!.socketFactory
if (!config.verifySSLHostname) hostnameVerifier = HostnameVerifier { _, _ -> true }
}
configure(chunked = request.hasDocument())
try {
request.write(outputStream)
val responseContentStream = try {
validateHttpResponse(request, inputStream)
inputStream
} catch (ioException: IOException) {
validateHttpResponse(request, errorStream, ioException)
errorStream
}
return decodeContentStream(request, responseContentStream)
.apply { httpServer = headerFields["Server"]?.first() }
} finally {
if (disconnectAfterHttpPost) disconnect()
}
}
}
private fun validateIppResponse(request: IppRequest, response: IppResponse) = response.run {
if (status == ClientErrorBadRequest) {
request.log(logger, SEVERE, prefix = "REQUEST: ")
response.log(logger, SEVERE, prefix = "RESPONSE: ")
}
if (containsGroup(Unsupported)) unsupportedGroup.values.forEach { logger.warning() { "Unsupported: $it" } }
if (!isSuccessful()) {
IppRegistrationsSection2.validate(request)
if (throwWhenNotSuccessful) {
throw if (status == ClientErrorNotFound) ClientErrorNotFoundException(request, response)
else IppOperationException(request, response)
}
}
}
internal fun toHttpUri(ippUri: URI): URI = with(ippUri) {
val scheme = scheme.replace("ipp", "http")
val port = if (port == -1) 631 else port
URI.create("$scheme:https://$host:$port$rawPath")
}
private fun HttpURLConnection.configure(chunked: Boolean) {
config.run {
connectTimeout = timeout.toMillis().toInt()
readTimeout = timeout.toMillis().toInt()
userAgent?.let { setRequestProperty("User-Agent", it) }
if (password != null) setRequestProperty("Authorization", authorization())
}
doOutput = true // POST
if (chunked) setChunkedStreamingMode(0)
setRequestProperty("Content-Type", APPLICATION_IPP)
setRequestProperty("Accept", APPLICATION_IPP)
setRequestProperty("Accept-Encoding", "identity") // avoid 'gzip' with Androids OkHttp
}
private fun HttpURLConnection.validateHttpResponse(
request: IppRequest,
contentStream: InputStream?,
exception: Exception? = null
) = when {
responseCode == 401 && request.operationGroup.containsKey("requesting-user-name") -> with(request) {
"User \"$requestingUserName\" is not authorized for operation $operation on $printerOrJobUri"
}
responseCode == 401 -> with(request) { "Not authorized for operation $operation on $printerOrJobUri (userName required)" }
responseCode == 426 -> "HTTP status $responseCode, $responseMessage, Try ipps:https://${request.printerOrJobUri.host}"
responseCode != 200 -> "HTTP request failed: $responseCode, $responseMessage"
contentType != null && !contentType.startsWith(APPLICATION_IPP) -> "Invalid Content-Type: $contentType"
exception != null -> exception.message
else -> null // no issues found
}?.let {
throw HttpPostException(
request,
httpStatus = responseCode,
httpHeaderFields = headerFields,
httpStream = contentStream,
message = it,
cause = exception
)
}
private fun decodeContentStream(request: IppRequest, contentStream: InputStream) = IppResponse().apply {
try {
read(contentStream)
} catch (throwable: Throwable) {
throw IppOperationException(request, this, message = "Failed to decode ipp response", cause = throwable)
.also {
if (onExceptionSaveMessages)
it.saveMessages("decoding_ipp_response_${request.requestId}_failed")
}
}
}
}