diff --git a/build.gradle.kts b/build.gradle.kts index 29eca917..addf4dae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,9 +52,10 @@ tasks.compileKotlin { // avoid warnings "jvm target compatibility should be set to the same Java version." tasks.compileTestKotlin { kotlinOptions { - jvmTarget = "11" + jvmTarget = "1.8" } } + tasks.compileJava { sourceCompatibility = tasks.compileKotlin.get().kotlinOptions.jvmTarget targetCompatibility = tasks.compileKotlin.get().kotlinOptions.jvmTarget diff --git a/src/main/kotlin/de/gmuth/http/Http.kt b/src/main/kotlin/de/gmuth/http/Http.kt deleted file mode 100644 index 32978bf8..00000000 --- a/src/main/kotlin/de/gmuth/http/Http.kt +++ /dev/null @@ -1,66 +0,0 @@ -package de.gmuth.http - -/** - * Copyright (c) 2020-2023 Gerhard Muth - */ - -import de.gmuth.http.Http.Implementation.JavaHttpURLConnection -import java.io.InputStream -import java.io.OutputStream -import java.net.URI -import java.nio.charset.Charset -import java.util.* -import javax.net.ssl.SSLContext - -interface Http { - - data class Config( - var timeout: Int = 30000, // milli seconds - var userAgent: String? = null, - var basicAuth: BasicAuth? = null, - var sslContext: SSLContext? = null, - // trust any certificate: sslContextForAnyCertificate() - // use individual certificate: sslContext(loadCertificate(FileInputStream("printer.pem"))) - // use truststore: sslContext(loadKeyStore(FileInputStream("printer.jks"), "changeit")) - var verifySSLHostname: Boolean = true, - var accept: String? = "application/ipp", // avoid 'text/html' with sun.net.www.protocol.http.HttpURLConnection - var acceptEncoding: String? = "identity", // avoid 'gzip' with Androids OkHttp - var debugLogging: Boolean = false - ) { - fun trustAnyCertificateAndSSLHostname() { - sslContext = SSLHelper.sslContextForAnyCertificate() - verifySSLHostname = false - } - } - - // https://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication - data class BasicAuth(val user: String, val password: String, val charset: Charset = Charsets.UTF_8) { - - fun encodeBase64(): String = Base64.getEncoder() - .encodeToString("$user:$password".toByteArray(charset)) - - fun authorization() = - "Basic " + encodeBase64() - } - - data class Response( - val status: Int, val server: String?, val contentType: String?, val contentStream: InputStream? - ) - - abstract class Client(val config: Config) { - abstract fun post( - uri: URI, contentType: String, writeContent: (OutputStream) -> Unit, chunked: Boolean = false - ): Response - } - - // standard jvm implementations - enum class Implementation(val createClient: (config: Config) -> Client) { - JavaHttpURLConnection({ HttpURLConnectionClient(it) }), - Java11HttpClient({ JavaHttpClient(it) }) - } - - companion object { - var defaultImplementation: Implementation = JavaHttpURLConnection - } - -} \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/http/HttpURLConnectionClient.kt b/src/main/kotlin/de/gmuth/http/HttpURLConnectionClient.kt deleted file mode 100644 index c5bd1372..00000000 --- a/src/main/kotlin/de/gmuth/http/HttpURLConnectionClient.kt +++ /dev/null @@ -1,65 +0,0 @@ -package de.gmuth.http - -/** - * Copyright (c) 2020-2023 Gerhard Muth - */ - -import java.io.OutputStream -import java.net.HttpURLConnection -import java.net.URI -import java.util.logging.Level -import java.util.logging.Logger.getLogger -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.HttpsURLConnection - -class HttpURLConnectionClient(config: Http.Config = Http.Config()) : Http.Client(config) { - - val log = getLogger(javaClass.name) - - init { - log.fine { "HttpURLConnectionClient created" } - if (config.debugLogging) { - getLogger("sun.net.www.protocol.http.HttpURLConnection").level = Level.FINER - } - } - - override fun post( - uri: URI, contentType: String, writeContent: (OutputStream) -> Unit, chunked: Boolean - ): Http.Response { - with(uri.toURL().openConnection() as HttpURLConnection) { - if (this is HttpsURLConnection && config.sslContext != null) { - sslSocketFactory = config.sslContext!!.socketFactory - if (!config.verifySSLHostname) hostnameVerifier = HostnameVerifier { _, _ -> true } - } - doOutput = true // trigger POST method - config.run { - connectTimeout = timeout - readTimeout = timeout - accept?.let { setRequestProperty("Accept", it) } - acceptEncoding?.let { setRequestProperty("Accept-Encoding", it) } - basicAuth?.let { setRequestProperty("Authorization", it.authorization()) } - userAgent?.let { setRequestProperty("User-Agent", it) } - } - setRequestProperty("Content-Type", contentType) - if (chunked) setChunkedStreamingMode(0) - writeContent(outputStream) - for ((key, values) in headerFields) { - val logLevel = when { - responseCode < 300 -> Level.FINE - responseCode in 400..499 -> Level.INFO - else -> Level.WARNING - } - log.log(logLevel) { "$key = $values" } - } - val responseStream = try { - inputStream - } catch (exception: Exception) { - log.severe { "http exception: $responseCode $responseMessage" } - errorStream - } - return Http.Response( - responseCode, getHeaderField("Server"), getHeaderField("Content-Type"), responseStream - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/http/JavaHttpClient.kt b/src/main/kotlin/de/gmuth/http/JavaHttpClient.kt deleted file mode 100644 index 09d5b912..00000000 --- a/src/main/kotlin/de/gmuth/http/JavaHttpClient.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.gmuth.http - -/** - * Copyright (c) 2020-2023 Gerhard Muth - */ - -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.OutputStream -import java.lang.System.setProperty -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpRequest.BodyPublishers -import java.net.http.HttpResponse.BodyHandlers -import java.time.Duration -import java.util.logging.Level -import java.util.logging.Logger.getLogger - -// requires Java >=11 -class JavaHttpClient(config: Http.Config = Http.Config()) : Http.Client(config) { - - companion object { - val log = getLogger(JavaHttpClient::javaClass.name) - fun isSupported() = try { - HttpClient.newHttpClient() - true - } catch (exception: ClassNotFoundException) { - log.log(Level.FINER, exception, { "HttpClient not found" }) - false - }.apply { - log.fine { "Java HttpClient supported: $this" } - } - } - - init { - log.fine { "JavaHttpClient created" } - if (!config.verifySSLHostname) - setProperty("jdk.internal.httpclient.disableHostnameVerification", true.toString()) - } - - val httpClient by lazy { - HttpClient.newBuilder().run { - config.sslContext?.let { sslContext(it) } - build() - } - } - - override fun post( - uri: URI, - contentType: String, - writeContent: (OutputStream) -> Unit, - chunked: Boolean - ): Http.Response { - val content = ByteArrayOutputStream().also { writeContent(it) }.toByteArray() - val request = HttpRequest.newBuilder().run { - with(config) { - timeout(Duration.ofMillis(timeout.toLong())) - userAgent?.let { header("User-Agent", it) } - acceptEncoding?.let { header("Accept-Encoding", it) } - basicAuth?.let { header("Authorization", it.authorization()) } - } - header("Content-Type", contentType) - POST(BodyPublishers.ofInputStream { ByteArrayInputStream(content) }) - uri(uri) - build() - } - httpClient.send(request, BodyHandlers.ofInputStream()).run { - return Http.Response( - statusCode(), - headers().firstValue("server").run { if (isPresent) get() else null }, - headers().firstValue("content-type").get(), - body() - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt b/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt index 499def9c..775a7b51 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt @@ -4,7 +4,6 @@ package de.gmuth.ipp.client * Copyright (c) 2020-2023 Gerhard Muth */ -import de.gmuth.http.Http import de.gmuth.ipp.client.IppExchangeException.ClientErrorNotFoundException import de.gmuth.ipp.core.IppOperation import de.gmuth.ipp.core.IppOperation.* @@ -21,23 +20,19 @@ import java.util.logging.Logger.getLogger // https://www.cups.org/doc/spec-ipp.html open class CupsClient( val cupsUri: URI = URI.create("ipp://localhost"), - val ippConfig: IppConfig = IppConfig(), - httpClient: Http.Client = Http.defaultImplementation.createClient(Http.Config()) + val ippClient: IppClient = IppClient() ) { constructor(host: String = "localhost") : this(URI.create("ipp://$host")) val log = getLogger(javaClass.name) - var userName: String? by ippConfig::userName - val httpConfig: Http.Config by httpClient::config + val config: IppConfig by ippClient::config + var userName: String? by config::userName var cupsClientWorkDirectory = File("cups-${cupsUri.host}") - val ippClient = IppClient(ippConfig, httpClient = httpClient) init { - if (cupsUri.scheme == "ipps") httpConfig.trustAnyCertificateAndSSLHostname() + if (cupsUri.scheme == "ipps") config.trustAnyCertificateAndSSLHostname() } - fun getIppServer() = ippClient.getHttpServer() - fun getPrinters() = try { exchange(ippRequest(CupsGetPrinters)) .getAttributesGroups(Printer) @@ -326,14 +321,14 @@ open class CupsClient( } tryToGetDocuments() if (ippExchangeException != null && ippExchangeException!!.httpStatus == 401) { - val configuredUserName = ippConfig.userName + val configuredUserName = config.userName val jobOwnersIterator = jobOwners.iterator() while (jobOwnersIterator.hasNext() && ippExchangeException != null) { - ippConfig.userName = jobOwnersIterator.next() - log.fine { "set userName '${ippConfig.userName}'" } + config.userName = jobOwnersIterator.next() + log.fine { "set userName '${config.userName}'" } tryToGetDocuments() } - ippConfig.userName = configuredUserName + config.userName = configuredUserName } documents.onEach { document -> document.save(job.printerDirectory(), overwrite = true) diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt b/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt index a19d7c94..db66adbf 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt @@ -4,7 +4,6 @@ package de.gmuth.ipp.client * Copyright (c) 2020-2023 Gerhard Muth */ -import de.gmuth.http.Http import de.gmuth.ipp.client.IppExchangeException.ClientErrorNotFoundException import de.gmuth.ipp.core.IppException import de.gmuth.ipp.core.IppOperation @@ -15,48 +14,32 @@ 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.net.HttpURLConnection import java.net.URI import java.util.concurrent.atomic.AtomicInteger -import java.util.logging.Level.FINE -import java.util.logging.Level.WARNING +import java.util.logging.Level.SEVERE 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(), - val httpConfig: Http.Config = Http.Config(), - val httpClient: Http.Client = Http.defaultImplementation.createClient(httpConfig) -) { +open class IppClient(val config: IppConfig = IppConfig()) { val log = getLogger(javaClass.name) var saveMessages: Boolean = false var saveMessagesDirectory = File("ipp-messages") var responseInterceptor: IppResponseInterceptor? = null + //var server: String? = null fun basicAuth(user: String, password: String) { - httpConfig.basicAuth = Http.BasicAuth(user, password) config.userName = user + config.password = password } companion object { const val APPLICATION_IPP = "application/ipp" - const val version = "3.0-SNAPSHOT" - const val build = "09-2023" - - init { - println("IPP-Client: Version: $version, Build: $build, MIT License, (c) 2020-2023 Gerhard Muth") - } - } - - init { - with(httpConfig) { if (userAgent == null) userAgent = "ipp-client/$version" } } - private var httpServer: String? = null - - @SuppressWarnings("kotlin:S6512") // read only - fun getHttpServer() = httpServer - //----------------- // build IppRequest //----------------- @@ -87,13 +70,10 @@ open class IppClient( open fun exchange(request: IppRequest, throwWhenNotSuccessful: Boolean = true): IppResponse { val ippUri: URI = request.printerUri - val httpUri = toHttpUri(ippUri) log.finer { "send '${request.operation}' request to $ippUri" } - val httpResponse = httpPostRequest(httpUri, request) - val response = decodeIppResponse(request, httpResponse) + val response = postRequest(toHttpUri(ippUri), request) log.fine { "$ippUri: $request => $response" } - httpServer = httpResponse.server if (saveMessages) { val messageSubDirectory = File(saveMessagesDirectory, ippUri.host).apply { @@ -107,11 +87,15 @@ open class IppClient( responseInterceptor?.invoke(request, response) - if (!response.isSuccessful()) { - IppRegistrationsSection2.validate(request) - if (throwWhenNotSuccessful) - throw if (response.status == ClientErrorNotFound) ClientErrorNotFoundException(request, response) - else IppExchangeException(request, response) + with(response) { + if (status == ClientErrorBadRequest) request.log(log, SEVERE, prefix = "BAD-REQUEST: ") + if (containsGroup(Unsupported)) unsupportedGroup.values.forEach { log.warning() { "unsupported: $it" } } + if (!isSuccessful()) { + IppRegistrationsSection2.validate(request) + if (throwWhenNotSuccessful) + throw if (status == ClientErrorNotFound) ClientErrorNotFoundException(request, response) + else IppExchangeException(request, response) + } } return response } @@ -122,49 +106,65 @@ open class IppClient( URI.create("$scheme://$host:$port$rawPath") } - fun httpPostRequest(httpUri: URI, request: IppRequest) = httpClient.post( - httpUri, APPLICATION_IPP, - { httpPostStream -> request.write(httpPostStream) }, - chunked = request.hasDocument() - ).apply { - var exceptionMessage: String? = null - if (contentType == null) { - log.fine { "missing content-type in http response (should be '$APPLICATION_IPP')" } - } else { - if (!contentType.startsWith(APPLICATION_IPP)) { - exceptionMessage = "invalid content-type: $contentType (expecting '$APPLICATION_IPP')" + open fun postRequest(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 } + } + 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 + setRequestProperty("Content-Type", APPLICATION_IPP) + setRequestProperty("Accept", APPLICATION_IPP) + setRequestProperty("Accept-Encoding", "identity") // avoid 'gzip' with Androids OkHttp + if (request.hasDocument()) setChunkedStreamingMode(0) // send document in chunks + request.write(outputStream) + val responseContentStream = try { + inputStream + } catch (throwable: Throwable) { + errorStream } - } - if (status != 200) exceptionMessage = "http request to $httpUri failed: status=$status" - if (status == 401) exceptionMessage = with(request) { - "user '$requestingUserName' is unauthorized for operation '$operation' (status=$status)" - } - exceptionMessage?.run { - config.log(log, WARNING) - request.log(log, WARNING, prefix = "IPP REQUEST: ") - log.warning { "http response status: $status" } - server?.let { log.warning { "ipp-server: $it" } } - contentType?.let { log.warning { "content-type: $it" } } - contentStream?.let { log.warning { "content:\n" + it.bufferedReader().use { it.readText() } } } - throw IppExchangeException(request, null, status, message = exceptionMessage) - } - } - fun decodeIppResponse(request: IppRequest, httpResponse: Http.Response) = IppResponse().apply { - try { - read(httpResponse.contentStream!!) - } catch (exception: Exception) { - throw IppExchangeException( - request, this, httpResponse.status, "failed to decode ipp response", exception - ).apply { - saveMessages("decoding_ipp_response_${request.requestId}_failed") + // error handling + when { + responseCode == 401 -> with(request) { + "User '$requestingUserName' is unauthorized for operation '$operation'" + } + responseCode != 200 -> { + "HTTP request to $httpUri failed: $responseCode, $responseMessage" + } + contentType != null && !contentType.startsWith(APPLICATION_IPP) -> { + "Invalid Content-Type: $contentType" + } + else -> null + }?.let { + throw IppExchangeException( + request, + response = null, + responseCode, + httpHeaderFields = headerFields, + httpStream = responseContentStream, + message = it + ) + } + + // decode ipp message + return IppResponse().apply { + try { + read(responseContentStream) + } catch (throwable: Throwable) { + throw IppExchangeException( + request, this, responseCode, message = "failed to decode ipp response", cause = throwable + ).apply { + saveMessages("decoding_ipp_response_${request.requestId}_failed") + } + } } - } - if (status == ClientErrorBadRequest) request.log(log, FINE, prefix="BAD-REQUEST: ") - if (!status.isSuccessful()) log.fine { "status: $status" } - if (hasStatusMessage()) log.fine { "status-message: $statusMessage" } - if (containsGroup(Unsupported)) unsupportedGroup.values.forEach { - log.warning { "unsupported: $it" } } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppConfig.kt b/src/main/kotlin/de/gmuth/ipp/client/IppConfig.kt index de856b0d..4e0ee798 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppConfig.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppConfig.kt @@ -5,20 +5,47 @@ package de.gmuth.ipp.client */ import java.nio.charset.Charset +import java.time.Duration +import java.util.Base64.getEncoder import java.util.logging.Level import java.util.logging.Level.INFO import java.util.logging.Logger +import javax.net.ssl.SSLContext class IppConfig( + + // core IPP config options var userName: String? = System.getProperty("user.name"), var ippVersion: String = "1.1", var charset: Charset = Charsets.UTF_8, var naturalLanguage: String = "en", + + // HTTP config options + var timeout: Duration = Duration.ofSeconds(30), + var userAgent: String? = "ipp-client/3.0", + var password: String? = null, + var sslContext: SSLContext? = null, + // trust any certificate: sslContextForAnyCertificate() + // use individual certificate: sslContext(loadCertificate(FileInputStream("printer.pem"))) + // use truststore: sslContext(loadKeyStore(FileInputStream("printer.jks"), "changeit")) + var verifySSLHostname: Boolean = true + ) { + fun authorization() = + "Basic " + getEncoder().encodeToString("$userName:$password".toByteArray(Charsets.UTF_8)) + + fun trustAnyCertificateAndSSLHostname() { + sslContext = SSLHelper.sslContextForAnyCertificate() + verifySSLHostname = false + } + fun log(logger: Logger, level: Level = INFO) = logger.run { log(level) { "userName: $userName" } log(level) { "ippVersion: $ippVersion" } log(level) { "charset: ${charset.name().lowercase()}" } log(level) { "naturalLanguage: $naturalLanguage" } + log(level) { "timeout: $timeout" } + log(level) { "userAgent: $userAgent" } + log(level) { "verifySSLHostname: $verifySSLHostname" } } } \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppExchangeException.kt b/src/main/kotlin/de/gmuth/ipp/client/IppExchangeException.kt index 5b2ecd8b..ccfa6f05 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppExchangeException.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppExchangeException.kt @@ -10,6 +10,7 @@ import de.gmuth.ipp.core.IppResponse import de.gmuth.ipp.core.IppStatus import de.gmuth.ipp.core.IppStatus.ClientErrorNotFound import java.io.File +import java.io.InputStream import java.util.logging.Level import java.util.logging.Level.INFO import java.util.logging.Logger @@ -21,9 +22,10 @@ open class IppExchangeException( val request: IppRequest, val response: IppResponse? = null, val httpStatus: Int? = null, + val httpHeaderFields: Map>? = null, + val httpStream: InputStream? = null, message: String = defaultMessage(request, response), - cause: Exception? = null - + cause: Throwable? = null, ) : IppException(message, cause) { class ClientErrorNotFoundException(request: IppRequest, response: IppResponse) : @@ -37,13 +39,14 @@ open class IppExchangeException( val log = getLogger(javaClass.name) companion object { - fun defaultMessage(request: IppRequest, response: IppResponse?) = StringBuilder().apply { + fun defaultMessage(request: IppRequest, response: IppResponse?) = StringBuilder().run { append("${request.operation} failed") response?.run { append(": '$status'") if (hasStatusMessage()) append(", $statusMessage") } - }.toString() + toString() + } } init { @@ -53,9 +56,12 @@ open class IppExchangeException( fun statusIs(status: IppStatus) = response?.status == status fun log(logger: Logger, level: Level = INFO) = logger.run { - if (httpStatus != null) log(level) { "HTTP-STATUS: $httpStatus" } - request.log(log, level, prefix = " REQUEST: ") + log(level) { message } + request.log(log, level, prefix = "REQUEST: ") response?.log(log, level, prefix = "RESPONSE: ") + httpStatus?.let { log(level) { "HTTP-Status: $it" } } + httpHeaderFields?.let { for ((key: String?, value) in it) log(level) { "HTTP: $key = $value" } } + httpStream?.let { log.log(level) { "HTTP-Content:\n" + it.bufferedReader().use { it.readText() } } } } fun saveMessages( diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt b/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt index 931296a8..95704575 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt @@ -4,7 +4,6 @@ package de.gmuth.ipp.client * Copyright (c) 2020-2023 Gerhard Muth */ -import de.gmuth.http.Http import de.gmuth.ipp.client.IppPrinterState.* import de.gmuth.ipp.core.* import de.gmuth.ipp.core.IppOperation.* @@ -18,19 +17,19 @@ import java.util.logging.Level import java.util.logging.Level.* import java.util.logging.Logger import java.util.logging.Logger.getLogger +import kotlin.io.path.createTempDirectory @SuppressWarnings("kotlin:S1192") -class IppPrinter( +open class IppPrinter( val printerUri: URI, var attributes: IppAttributesGroup = IppAttributesGroup(Printer), - httpConfig: Http.Config = Http.Config(), ippConfig: IppConfig = IppConfig(), - val ippClient: IppClient = IppClient(ippConfig, httpConfig), + val ippClient: IppClient = IppClient(ippConfig), getPrinterAttributesOnInit: Boolean = true, requestedAttributesOnInit: List? = null ) { - var workDirectory: File = File("work") val log = getLogger(javaClass.name) + var workDirectory: File = createTempDirectory().toFile() companion object { @@ -41,9 +40,12 @@ class IppPrinter( val printerClassAttributes = listOf( "printer-name", "printer-make-and-model", + "printer-info", + "printer-location", "printer-is-accepting-jobs", "printer-state", "printer-state-reasons", + "printer-state-message", "document-format-supported", "operations-supported", "color-supported", @@ -57,7 +59,7 @@ class IppPrinter( init { log.fine { "create IppPrinter for $printerUri" } - if (printerUri.scheme == "ipps") httpConfig.trustAnyCertificateAndSSLHostname() + if (printerUri.scheme == "ipps") ippConfig.trustAnyCertificateAndSSLHostname() if (!getPrinterAttributesOnInit) { log.fine { "getPrinterAttributesOnInit disabled => no printer attributes available" } } else if (attributes.isEmpty()) { @@ -77,11 +79,6 @@ class IppPrinter( if (it.containsGroup(Printer)) log.info { "${it.printerGroup.size} attributes parsed" } else log.warning { it.toString() } } - try { - fetchRawPrinterAttributes("getPrinterAttributesFailed.bin") - } catch (exception: Exception) { - log.log(SEVERE, exception, { "failed to fetch raw printer attributes" }) - } } throw ippExchangeException } @@ -591,17 +588,6 @@ class IppPrinter( } } - fun fetchRawPrinterAttributes(filename: String = "printer-attributes.bin") { - ippClient.run { - val httpResponse = httpPostRequest(toHttpUri(printerUri), ippRequest(GetPrinterAttributes)) - log.info { "http status: ${httpResponse.status}, content-type: ${httpResponse.contentType}" } - File(filename).apply { - httpResponse.contentStream!!.copyTo(outputStream()) - log.info { "saved ${length()} bytes: $path" } - } - } - } - fun printerDirectory(printerName: String = name.text.replace("\\s+".toRegex(), "_")) = File(workDirectory, printerName).apply { if (!mkdirs() && !isDirectory) throw IOException("failed to create printer directory: $path") diff --git a/src/main/kotlin/de/gmuth/http/SSLHelper.kt b/src/main/kotlin/de/gmuth/ipp/client/SSLHelper.kt similarity index 98% rename from src/main/kotlin/de/gmuth/http/SSLHelper.kt rename to src/main/kotlin/de/gmuth/ipp/client/SSLHelper.kt index 11d86379..df816dfe 100644 --- a/src/main/kotlin/de/gmuth/http/SSLHelper.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/SSLHelper.kt @@ -1,4 +1,4 @@ -package de.gmuth.http +package de.gmuth.ipp.client /** * Copyright (c) 2020-2023 Gerhard Muth diff --git a/src/main/kotlin/de/gmuth/ipp/core/IppException.kt b/src/main/kotlin/de/gmuth/ipp/core/IppException.kt index 38fc80b8..78c3377f 100644 --- a/src/main/kotlin/de/gmuth/ipp/core/IppException.kt +++ b/src/main/kotlin/de/gmuth/ipp/core/IppException.kt @@ -1,7 +1,7 @@ package de.gmuth.ipp.core /** - * Copyright (c) 2020-2022 Gerhard Muth + * Copyright (c) 2020-2023 Gerhard Muth */ open class IppException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/src/main/kotlin/de/gmuth/ipp/core/IppResponse.kt b/src/main/kotlin/de/gmuth/ipp/core/IppResponse.kt index 174b7f52..645286b2 100644 --- a/src/main/kotlin/de/gmuth/ipp/core/IppResponse.kt +++ b/src/main/kotlin/de/gmuth/ipp/core/IppResponse.kt @@ -1,7 +1,7 @@ package de.gmuth.ipp.core /** - * Copyright (c) 2020-2021 Gerhard Muth + * Copyright (c) 2020-2023 Gerhard Muth */ import de.gmuth.ipp.core.IppTag.Printer @@ -24,7 +24,7 @@ class IppResponse : IppMessage { get() = operationGroup.getValue("status-message") fun hasStatusMessage() = - operationGroup.containsKey("status-message") + operationGroup.containsKey("status-message") val printerGroup: IppAttributesGroup get() = getSingleAttributesGroup(Printer) @@ -37,11 +37,11 @@ class IppResponse : IppMessage { constructor() : super() constructor( - status: IppStatus, - version: String = "1.1", - requestId: Int = 1, - charset: Charset = Charsets.UTF_8, - naturalLanguage: String = "en" + status: IppStatus, + version: String = "1.1", + requestId: Int = 1, + charset: Charset = Charsets.UTF_8, + naturalLanguage: String = "en" ) : super(version, requestId, charset, naturalLanguage) { code = status.code } diff --git a/src/test/kotlin/de/gmuth/http/HttpClientMock.kt b/src/test/kotlin/de/gmuth/http/HttpClientMock.kt deleted file mode 100644 index c5e29c27..00000000 --- a/src/test/kotlin/de/gmuth/http/HttpClientMock.kt +++ /dev/null @@ -1,39 +0,0 @@ -package de.gmuth.http - -/** - * Copyright (c) 2021 Gerhard Muth - */ - -import de.gmuth.io.ByteArray -import de.gmuth.ipp.core.IppResponse -import de.gmuth.ipp.core.IppStatus -import java.io.* -import java.net.URI -import java.util.logging.Logger.getLogger - -class HttpClientMock(config: Http.Config = Http.Config()) : Http.Client(config) { - - val log = getLogger(javaClass.name) - lateinit var rawIppRequest: ByteArray - var httpStatus: Int = 200 - var httpServer: String? = "HttpClientMock" - var httpContentType: String? = "application/ipp" - var ippResponse: IppResponse? = IppResponse(IppStatus.SuccessfulOk) - var httpContentFile: File? = null - - fun mockResponse(file: File) { - httpContentFile = file - ippResponse = null - } - - fun mockResponse(fileName: String, directory: String = "printers") = - mockResponse(File(directory, fileName)) - - override fun post(uri: URI, contentType: String, writeContent: (OutputStream) -> Unit, chunked: Boolean) = - Http.Response( - httpStatus, httpServer, httpContentType, ippResponse?.encodeAsInputStream() ?: httpContentFile?.inputStream() - ).apply { - rawIppRequest = ByteArray(writeContent) - log.info { "post ${rawIppRequest.size} bytes ipp request to $uri -> response '$server', $status, ${this.contentType}" } - } -} \ No newline at end of file diff --git a/src/test/kotlin/de/gmuth/ipp/client/CupsClientTests.kt b/src/test/kotlin/de/gmuth/ipp/client/CupsClientTests.kt index 614e8f1a..8e397e36 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/CupsClientTests.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/CupsClientTests.kt @@ -1,46 +1,49 @@ package de.gmuth.ipp.client /** - * Copyright (c) 2020-2021 Gerhard Muth + * Copyright (c) 2020-2023 Gerhard Muth */ -import de.gmuth.http.HttpClientMock import de.gmuth.ipp.core.IppException -import de.gmuth.ipp.core.IppResponse import de.gmuth.ipp.core.IppStatus.ClientErrorNotFound -import de.gmuth.ipp.core.IppStatus.SuccessfulOk import org.junit.Test import java.net.URI import java.util.logging.Logger.getLogger +import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue class CupsClientTests { val log = getLogger(javaClass.name) - val httpClient = HttpClientMock() - val cupsClient = CupsClient(URI.create("ipps://cups"), httpClient = httpClient) - - init { - httpClient.ippResponse = IppResponse(SuccessfulOk) // default mocked response - } + val ippClientMock = IppClientMock("printers/CUPS") + val cupsClient = CupsClient(URI.create("ipps://cups"), ippClient = ippClientMock) @Test fun constructors() { CupsClient() CupsClient("host") - CupsClient(ippConfig = IppConfig()) } @Test fun getPrinters() { - httpClient.mockResponse("CUPS/Cups-Get-Printers.ipp") - cupsClient.getPrinters().forEach { log.info { it.toString() } } + ippClientMock.mockResponse("Cups-Get-Printers.ipp") + cupsClient.getPrinters().run { + forEach { log.info { it.toString() } } + assertEquals(12, size) + } } @Test fun getPrinter() { - httpClient.mockResponse("CUPS_HP_LaserJet_100_color_MFP_M175/Get-Printer-Attributes.ipp") - cupsClient.getPrinter("ColorJet_HP") + ippClientMock.mockResponse("Get-Printer-Attributes.ipp", "printers/CUPS_HP_LaserJet_100_color_MFP_M175") + cupsClient.getPrinter("ColorJet_HP").run { + log(log) + assertEquals("HP LaserJet 100 color MFP M175", makeAndModel.text) + assertEquals(IppPrinterState.Idle, state) + assertEquals(5, markers.size) + assertTrue(isAcceptingJobs) + assertTrue(isCups()) + } } @Test @@ -52,14 +55,16 @@ class CupsClientTests { @Test fun getDefault() { - httpClient.mockResponse("CUPS/Cups-Get-Default.ipp") - cupsClient.getDefault() + ippClientMock.mockResponse("Cups-Get-Default.ipp") + cupsClient.getDefault().run { + assertEquals("ColorJet_HP", name.text) + } } @Test fun getDefaultFails() { val exception = assertFailsWith { - httpClient.mockResponse("CUPS/Cups-Get-Default-Error.ipp") + ippClientMock.mockResponse("Cups-Get-Default-Error.ipp") cupsClient.getDefault() } assertTrue(exception.statusIs(ClientErrorNotFound)) diff --git a/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt b/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt new file mode 100644 index 00000000..15ac8b68 --- /dev/null +++ b/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt @@ -0,0 +1,51 @@ +package de.gmuth.ipp.client + +/** + * Copyright (c) 2023 Gerhard Muth + */ + +import de.gmuth.io.ByteArray +import de.gmuth.ipp.core.IppRequest +import de.gmuth.ipp.core.IppResponse +import de.gmuth.ipp.core.IppStatus +import de.gmuth.log.Logging +import java.io.File +import java.net.URI + +class IppClientMock( + var directory: String = "printers" +) : IppClient() { + + init { + Logging.configure() + mockResponse(IppResponse(IppStatus.SuccessfulOk)) + } + + lateinit var rawResponse: ByteArray + + fun mockResponse(response: IppResponse) { + rawResponse = response.encode() + } + + fun mockResponse(file: File) { + rawResponse = file.readBytes() + } + + fun mockResponse(fileName: String, directory: String = this.directory) { + mockResponse(File(directory, fileName)) + } + + // when used with real http, responses are frequently created and garbage collected + // however references to attribute groups are kept in IPP objects + // changes to an attribute group would affect other tests as well + // therefor it's important to produce a fresh response for each call + + override fun postRequest(httpUri: URI, request: IppRequest): IppResponse { + ByteArray { request.write(it) }.run { + log.info { "mocked post $size IPP bytes to $httpUri" } + } + return IppResponse().apply { + decode(rawResponse) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/de/gmuth/ipp/client/IppClientTests.kt b/src/test/kotlin/de/gmuth/ipp/client/IppClientTests.kt index e603d9c7..ce3f2edb 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/IppClientTests.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/IppClientTests.kt @@ -1,26 +1,18 @@ package de.gmuth.ipp.client -import de.gmuth.http.HttpClientMock import de.gmuth.ipp.core.IppOperation.GetPrinterAttributes -import de.gmuth.ipp.core.IppResponse -import de.gmuth.ipp.core.IppStatus.SuccessfulOk import org.junit.Test import java.net.URI import kotlin.test.assertEquals class IppClientTests { - val httpClient = HttpClientMock() - val ippClient = IppClient(httpClient = httpClient) - - init { - httpClient.ippResponse = IppResponse(SuccessfulOk) - } + val ippClient = IppClientMock() @Test fun sendRequestToURIWithEncodedWhitespaces() { - val request = ippClient.ippRequest(GetPrinterAttributes, URI.create("ipp://0/PDF%20Printer")) - ippClient.exchange(request) - assertEquals("/PDF%20Printer", request.printerUri.rawPath) - assertEquals("/PDF Printer", request.printerUri.path) + ippClient.ippRequest(GetPrinterAttributes, URI.create("ipp://0/PDF%20Printer")).run { + assertEquals("/PDF%20Printer", printerUri.rawPath) + assertEquals("/PDF Printer", printerUri.path) + } } } diff --git a/src/test/kotlin/de/gmuth/ipp/client/IppJobTests.kt b/src/test/kotlin/de/gmuth/ipp/client/IppJobTests.kt index 4f05c915..38ad3029 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/IppJobTests.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/IppJobTests.kt @@ -4,7 +4,6 @@ package de.gmuth.ipp.client * Copyright (c) 2021-2023 Gerhard Muth */ -import de.gmuth.http.HttpClientMock import de.gmuth.ipp.core.IppResponse import de.gmuth.ipp.core.IppStatus.SuccessfulOk import de.gmuth.ipp.core.IppTag @@ -15,7 +14,6 @@ import java.io.File import java.io.FileInputStream import java.net.URI import java.util.logging.Logger.getLogger -import kotlin.io.path.createTempDirectory import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -25,30 +23,20 @@ class IppJobTests { val log = getLogger(javaClass.name) val blankPdf = File("tool/A4-blank.pdf") - val httpClient = HttpClientMock() - val ippConfig = IppConfig() - val printer: IppPrinter - val job: IppJob - - init { - // mock ipp printer - printer = IppPrinter( - URI.create("ipp://printer"), - ippClient = IppClient(ippConfig, httpClient = httpClient), - getPrinterAttributesOnInit = false - ).apply { - attributes = ippResponse("Get-Printer-Attributes.ipp").printerGroup - workDirectory = createTempDirectory().toFile() + val ippClientMock = IppClientMock("printers/CUPS_HP_LaserJet_100_color_MFP_M175") + + val printer = IppPrinter( + URI.create("ipp://printer-for-job-tests"), + ippClient = ippClientMock.apply { mockResponse("Get-Printer-Attributes.ipp") } + ) + + val job: IppJob = printer.run { + ippClientMock.mockResponse("Get-Job-Attributes.ipp") + getJob(2366).apply { + attributes.attribute("document-name-supplied", NameWithLanguage, "blank.pdf".toIppString()) } - // mock ipp job - job = IppJob(printer, ippResponse("Get-Job-Attributes.ipp").jobGroup.apply { - attribute("document-name-supplied", NameWithLanguage, "blank.pdf".toIppString()) - }) } - fun ippResponse(fileName: String, directory: String = "printers/CUPS_HP_LaserJet_100_color_MFP_M175") = - IppResponse().apply { read(File(directory, fileName)) } - @Test fun jobAttributes() { job.apply { @@ -69,13 +57,11 @@ class IppJobTests { @Test fun getAttributes() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.getJobAttributes() } @Test fun updateAttributes() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.apply { updateAttributes() log(log) @@ -85,31 +71,26 @@ class IppJobTests { @Test fun hold() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.hold() } @Test fun release() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.release() } @Test fun restart() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.restart() } @Test fun cancel() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.cancel() } @Test fun cancelWithMessage() { - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") job.cancel("message") } @@ -137,20 +118,19 @@ class IppJobTests { assertFalse(isProcessingToStopPoint()) attributes.attribute("job-state-reasons", Keyword, "processing-to-stop-point") assertTrue(isProcessingToStopPoint()) - httpClient.ippResponse = ippResponse("Get-Job-Attributes.ipp") cancel() } } @Test fun sendDocument() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") job.sendDocument(FileInputStream(blankPdf)) } @Test fun sendUri() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") job.sendUri(URI.create("ftp://no.doc")) } @@ -187,7 +167,7 @@ class IppJobTests { @Test fun cupsGetDocument1() { - httpClient.ippResponse = cupsDocumentResponse("application/pdf") + ippClientMock.mockResponse(cupsDocumentResponse("application/pdf")) job.cupsGetDocument().apply { log.info { toString() } log(log) @@ -197,16 +177,16 @@ class IppJobTests { @Test fun cupsGetDocument2() { - httpClient.ippResponse = cupsDocumentResponse("application/postscript") + ippClientMock.mockResponse(cupsDocumentResponse("application/postscript")) job.cupsGetDocument().filename() } @Test fun cupsGetDocument3() { printer.attributes.remove("cups-version") - httpClient.ippResponse = cupsDocumentResponse("application/octetstream").apply { + ippClientMock.mockResponse(cupsDocumentResponse("application/octetstream").apply { jobGroup.remove("document-name") - } + }) job.cupsGetDocument(2).apply { log.info { toString() } log.info { "${filename()} (${readBytes().size} bytes)" } @@ -217,7 +197,7 @@ class IppJobTests { @Test fun cupsGetAndSaveDocuments() { - httpClient.ippResponse = cupsDocumentResponse("application/postscript") + ippClientMock.mockResponse(cupsDocumentResponse("application/postscript")) job.cupsGetDocuments(save = true) } diff --git a/src/test/kotlin/de/gmuth/ipp/client/IppPrinterTests.kt b/src/test/kotlin/de/gmuth/ipp/client/IppPrinterTests.kt index a435cad0..b2f056dc 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/IppPrinterTests.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/IppPrinterTests.kt @@ -1,11 +1,9 @@ package de.gmuth.ipp.client /** - * Copyright (c) 2021-2022 Gerhard Muth + * Copyright (c) 2021-2023 Gerhard Muth */ -import de.gmuth.http.HttpClientMock -import de.gmuth.io.toIppResponse import de.gmuth.ipp.client.CupsPrinterType.Capability.CanPunchOutput import de.gmuth.ipp.client.IppFinishing.Punch import de.gmuth.ipp.client.IppFinishing.Staple @@ -35,18 +33,11 @@ class IppPrinterTests { val tlog = getLogger(javaClass.name) val blankPdf = File("tool/A4-blank.pdf") - val httpClient = HttpClientMock() - val ippConfig = IppConfig() - + val ippClientMock = IppClientMock("printers/Simulated_Laser_Printer") val printer = IppPrinter( - URI.create("ipp://printer"), - ippClient = IppClient(ippConfig, httpClient = httpClient), - getPrinterAttributesOnInit = false - ).apply { - attributes = File("printers/Simulated_Laser_Printer/Get-Printer-Attributes.ipp") - .toIppResponse() - .printerGroup - } + URI.create("ipp://printer-for-printer-tests"), + ippClient = ippClientMock.apply { mockResponse("Get-Printer-Attributes.ipp") } + ) @Test fun printerAttributes() { @@ -93,14 +84,14 @@ class IppPrinterTests { @Test fun savePrinterAttributes() { - httpClient.mockResponse("Simulated_Laser_Printer/Get-Printer-Attributes.ipp") + ippClientMock.mockResponse("Get-Printer-Attributes.ipp") printer.savePrinterAttributes(createTempDirectory().pathString) } @Test fun updateAttributes() { - httpClient.mockResponse("Simulated_Laser_Printer/Get-Printer-Attributes.ipp") - printer.apply { + ippClientMock.mockResponse("Get-Printer-Attributes.ipp") + printer.run { updateAttributes() log(tlog) assertEquals(122, attributes.size) @@ -117,7 +108,7 @@ class IppPrinterTests { @Test fun printJobFile() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") printer.printJob( File("tool/A4-blank.pdf"), jobName("A4.pdf"), @@ -149,53 +140,57 @@ class IppPrinterTests { @Test fun printJobInputStream() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") - printer.printJob(FileInputStream(blankPdf)) + ippClientMock.mockResponse("Print-Job.ipp") + printer.printJob(FileInputStream(blankPdf)).run { + log(tlog) + assertEquals(461881017, id) + assertEquals(IppJobState.Pending, state) + assertEquals(listOf("none"), stateReasons) + assertEquals("ipp://SpaceBook-2.local.:8632/jobs/461881017", uri.toString()) + } } @Test fun printJobByteArray() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") printer.printJob(blankPdf.readBytes()) } @Test fun printUri() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") printer.printUri(URI.create("http://server/document.pdf")) } @Test fun createJob() { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") - printer.createJob().apply { - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") + ippClientMock.mockResponse("Print-Job.ipp") + printer.createJob().run { sendDocument(FileInputStream(blankPdf), true, "blank.pdf", "en") - httpClient.mockResponse("Simulated_Laser_Printer/Print-Job.ipp") sendUri(URI.create("http://server/document.pdf"), true, "black.pdf", "en") } } @Test fun getJob() { - httpClient.mockResponse("Simulated_Laser_Printer/Get-Job-Attributes.ipp") - printer.getJob(11).apply { + ippClientMock.mockResponse("Get-Job-Attributes.ipp") + printer.getJob(11).run { assertEquals(21, attributes.size) } } @Test fun getJobsWithDefaultParameters() { - httpClient.mockResponse("Simulated_Laser_Printer/Get-Jobs.ipp") - printer.getJobs().apply { + ippClientMock.mockResponse("Get-Jobs.ipp") + printer.getJobs().run { assertEquals(1, size) } } @Test fun getJobsWithParameters() { - httpClient.mockResponse("Simulated_Laser_Printer/Get-Jobs.ipp") - printer.getJobs(Completed, myJobs = true, limit = 10).apply { + ippClientMock.mockResponse("Get-Jobs.ipp") + printer.getJobs(Completed, myJobs = true, limit = 10).run { assertEquals(1, size) } } diff --git a/src/test/kotlin/de/gmuth/ipp/client/issueNo11.kt b/src/test/kotlin/de/gmuth/ipp/client/issueNo11.kt index 47cbe8fb..bf2001f3 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/issueNo11.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/issueNo11.kt @@ -1,6 +1,5 @@ package de.gmuth.ipp.client -import de.gmuth.http.Http import java.net.URI import java.util.logging.Level.SEVERE import java.util.logging.Logger.getLogger @@ -20,7 +19,7 @@ fun main() { with( IppPrinter( printerUri, - httpConfig = Http.Config(debugLogging = true), + //httpConfig = HttpClient.Config(debugLogging = true), //getPrinterAttributesOnInit = false ) ) {