View Javadoc
1   package org.metricshub.winrm.service.client.encryption;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 - 2024 Metricshub
8    * ჻჻჻჻჻჻
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21   */
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.net.URI;
28  import java.util.Arrays;
29  import java.util.List;
30  import org.apache.cxf.Bus;
31  import org.apache.cxf.io.CacheAndWriteOutputStream;
32  import org.apache.cxf.message.Message;
33  import org.apache.cxf.service.model.EndpointInfo;
34  import org.apache.cxf.transport.http.Address;
35  import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit;
36  import org.apache.cxf.transport.http.asyncclient.CXFHttpRequest;
37  import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
38  import org.apache.cxf.ws.addressing.EndpointReferenceType;
39  import org.apache.http.auth.Credentials;
40  import org.apache.http.client.config.AuthSchemes;
41  import org.apache.http.client.config.RequestConfig;
42  import org.apache.http.entity.BasicHttpEntity;
43  
44  /**
45   * Creates an output stream which sends back the appropriate encrypted or unencrypted stream,
46   * based on the {SignAndEncryptOutInterceptor} -- which normally does the right thing,
47   * but during auth events it will "guess" wrongly, and we have to change the payload and
48   * the headers. {io.cloudsoft.winrm4j.client.ntlm.NTCredentialsWithEncryption} will do
49   * that by finding the {@link EncryptionAwareHttpEntity}.
50   *
51   * Code from io.cloudsoft.winrm4j.client.encryption.AsyncHttpEncryptionAwareConduit
52   * release 0.12.3 @link https://github.com/cloudsoft/winrm4j
53   */
54  public class AsyncHttpEncryptionAwareConduit extends AsyncHTTPConduit {
55  
56  	static final byte[] PRE_AUTH_BOGUS_PAYLOAD = "AWAITING_ENCRYPTION_KEYS".getBytes();
57  
58  	private static final List<String> TARGET_AUTH_SCHEMES = Arrays.asList(AuthSchemes.SPNEGO, AuthSchemes.KERBEROS);
59  
60  	private static ContentWithType getAppropriate(final Message msg) {
61  		final EncryptAndSignOutputStream encryptingStream = msg.getContent(EncryptAndSignOutputStream.class);
62  		if (encryptingStream == null) {
63  			throw new IllegalStateException("No SignAndEncryptOutInterceptor applied to message");
64  		}
65  		return encryptingStream.getAppropriate();
66  	}
67  
68  	public AsyncHttpEncryptionAwareConduit(
69  		final Bus bus,
70  		final EndpointInfo endpointInfo,
71  		final EndpointReferenceType endpointReferenceType,
72  		final AsyncHttpEncryptionAwareConduitFactory factory
73  	) throws IOException {
74  		super(bus, endpointInfo, endpointReferenceType, factory);
75  	}
76  
77  	@Override
78  	protected OutputStream createOutputStream(
79  		final Message message,
80  		final boolean needToCacheRequest,
81  		final boolean isChunking,
82  		final int chunkThreshold
83  	) throws IOException {
84  		final NtlmEncryptionUtils encryptor = NtlmEncryptionUtils.of(message.get(Credentials.class));
85  		if (encryptor == null) {
86  			return super.createOutputStream(message, needToCacheRequest, isChunking, chunkThreshold);
87  		}
88  
89  		if (Boolean.TRUE.equals(message.get(USE_ASYNC))) {
90  			// copied from super, but for our class
91  			final CXFHttpRequest requestEntity = message.get(CXFHttpRequest.class);
92  			final AsyncWrappedEncryptionAwareOutputStream out = new AsyncWrappedEncryptionAwareOutputStream(
93  				message,
94  				true,
95  				false,
96  				chunkThreshold,
97  				getConduitName(),
98  				requestEntity.getURI()
99  			);
100 
101 			requestEntity.setOutputStream(out);
102 			return out;
103 		}
104 
105 		throw new IllegalStateException("Encryption only available with ASYNC at present");
106 		// if needed could also subclass the URL stream used by super.super.createOutput
107 	}
108 
109 	@Override
110 	protected void setupConnection(final Message message, final Address address, final HTTPClientPolicy csPolicy)
111 		throws IOException {
112 		super.setupConnection(message, address, csPolicy);
113 
114 		// replace similar logic in super method, but with a refreshHeaders method available
115 
116 		final CXFHttpRequest requestEntity = message.get(CXFHttpRequest.class);
117 
118 		final BasicHttpEntity entity = new EncryptionAwareHttpEntity() {
119 			@Override
120 			public boolean isRepeatable() {
121 				return requestEntity.getEntity().isRepeatable();
122 			}
123 
124 			@Override
125 			protected ContentWithType getAppropriate() {
126 				return AsyncHttpEncryptionAwareConduit.getAppropriate(message);
127 			}
128 		};
129 		entity.setChunked(true);
130 		entity.setContentType((String) message.get(Message.CONTENT_TYPE));
131 
132 		requestEntity.setEntity(entity);
133 
134 		requestEntity.setConfig(
135 			RequestConfig.copy(requestEntity.getConfig()).setTargetPreferredAuthSchemes(TARGET_AUTH_SCHEMES).build()
136 		);
137 	}
138 
139 	private class AsyncWrappedEncryptionAwareOutputStream extends AsyncWrappedOutputStream {
140 
141 		public AsyncWrappedEncryptionAwareOutputStream(
142 			final Message message,
143 			final boolean needToCacheRequest,
144 			final boolean isChunking,
145 			final int chunkThreshold,
146 			final String conduitName,
147 			final URI uri
148 		) {
149 			super(message, needToCacheRequest, isChunking, chunkThreshold, conduitName, uri);
150 		}
151 
152 		@Override
153 		protected void setupWrappedStream() throws IOException {
154 			super.setupWrappedStream();
155 
156 			if (!(cachedStream.getFlowThroughStream() instanceof EncryptionAwareCacheAndWriteOutputStream)) {
157 				cachedStream = new EncryptionAwareCacheAndWriteOutputStream(cachedStream.getFlowThroughStream());
158 				wrappedStream = cachedStream;
159 			}
160 		}
161 
162 		private class EncryptionAwareCacheAndWriteOutputStream extends CacheAndWriteOutputStream {
163 
164 			public EncryptionAwareCacheAndWriteOutputStream(OutputStream outbufFlowThroughStream) {
165 				super(outbufFlowThroughStream);
166 			}
167 
168 			@Override
169 			public byte[] getBytes() throws IOException {
170 				final ContentWithType appropriate = AsyncHttpEncryptionAwareConduit.getAppropriate(outMessage);
171 				return appropriate.getPayload();
172 			}
173 
174 			@Override
175 			public InputStream getInputStream() throws IOException {
176 				return new ByteArrayInputStream(getBytes());
177 			}
178 		}
179 	}
180 }