diff --git a/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy b/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy index 4d599898..c5a90789 100644 --- a/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy +++ b/src/it/groovy/com/hierynomus/smbj/SMB2FileIntegrationTest.groovy @@ -18,22 +18,31 @@ package com.hierynomus.smbj import com.hierynomus.msdtyp.AccessMask import com.hierynomus.mserref.NtStatus import com.hierynomus.mssmb2.SMB2CreateDisposition +import com.hierynomus.mssmb2.SMB2FileId +import com.hierynomus.mssmb2.SMB2OplockLevel import com.hierynomus.mssmb2.SMB2ShareAccess import com.hierynomus.mssmb2.SMBApiException -import com.hierynomus.smb.SMBPacket import com.hierynomus.smbj.auth.AuthenticationContext import com.hierynomus.smbj.connection.Connection +import com.hierynomus.smbj.event.AsyncCreateResponseNotification +import com.hierynomus.smbj.event.OplockBreakNotification +import com.hierynomus.smbj.event.handler.AbstractNotificationHandler +import com.hierynomus.smbj.event.handler.MessageIdCallback import com.hierynomus.smbj.io.ArrayByteChunkProvider import com.hierynomus.smbj.session.Session +import com.hierynomus.smbj.share.DiskEntry import com.hierynomus.smbj.share.DiskShare import com.hierynomus.smbj.transport.tcp.async.AsyncDirectTcpTransportFactory import spock.lang.Specification import spock.lang.Unroll import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import static com.hierynomus.mssmb2.SMB2CreateDisposition.FILE_CREATE import static com.hierynomus.mssmb2.SMB2CreateDisposition.FILE_OPEN +import static com.hierynomus.mssmb2.SMB2CreateDisposition.FILE_OPEN_IF class SMB2FileIntegrationTest extends Specification { @@ -185,4 +194,158 @@ class SMB2FileIntegrationTest extends Specification { cleanup: share.rm("bigfile") } + + def "should able to async create"() { + given: + def path = "createAsync.txt" + // In actual implementation, the path is not available for createResponse complete. Map is required. + def messageIdPathMap = new ConcurrentHashMap() + // Should call async listener, just calling dummy in test case + def testSucceed = new AtomicBoolean(false) + share.setNotificationHandler( new AbstractNotificationHandler() { + + @Override + void handleAsyncCreateResponseNotification( + AsyncCreateResponseNotification asyncCreateResponseNotification) { + def createResponse = asyncCreateResponseNotification.createResponse + def getPath = messageIdPathMap.remove(createResponse.header.messageId) + if(getPath == null) { + System.out.println("Could not find path in map. Should not related to async create, ignored.") + return + } + + if(createResponse.header.status != NtStatus.STATUS_SUCCESS) { + throw new IllegalStateException("Async create failed with status " + createResponse.header.status.value) + } + + def diskEntry = share.getDiskEntry(getPath, new DiskShare.SMB2CreateResponseContext(createResponse, share)) + + if(diskEntry != null) { + // Should call async listener, just calling dummy in test case + testSucceed.compareAndSet(false, true) + } + } + + }) + + when: + share.openAsync(path, null, null, EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_CREATE, null, new MessageIdCallback() { + + @Override + void callback(long messageId) { + messageIdPathMap.put(messageId, path) + } + }) + + then: + // 1 second should be enough for the whole process complete in docker + Thread.sleep(1000L) + + expect: + testSucceed.get() == true + + cleanup: + share.rm(path) + messageIdPathMap.clear() + + } + + def "should able to receive oplock break notification and response acknowledgement then receive acknowledgement response"() { + given: + def path = "createAsyncOplock.txt" + // In actual implementation, the path is not available for createResponse complete. Map is required. + def messageIdPathMap = new ConcurrentHashMap() + // Should call async listener, just using hashmap as dummy in test case + def messageIdDiskEntryMap = new ConcurrentHashMap() + def fileIdDiskEntryMap = new ConcurrentHashMap() + def succeedBreakToLevel2 = new AtomicBoolean(false) + def oplockBreakAcknowledgmentResponseSucceed = new AtomicBoolean(false) + share.setNotificationHandler( new AbstractNotificationHandler() { + + @Override + void handleAsyncCreateResponseNotification( + AsyncCreateResponseNotification asyncCreateResponseNotification) { + def createResponse = asyncCreateResponseNotification.createResponse + def getPath = messageIdPathMap.remove(createResponse.header.messageId) + if(getPath == null) { + System.out.println("Could not find path in map. Should not related to async create, ignored.") + return + } + + if(createResponse.header.status != NtStatus.STATUS_SUCCESS) { + throw new IllegalStateException("Async create failed with status " + createResponse.header.status.value) + } + + def diskEntry = share.getDiskEntry(getPath, new DiskShare.SMB2CreateResponseContext(createResponse, share)) + + if(diskEntry != null) { + // Should call async listener, just calling dummy in test case + messageIdDiskEntryMap.put(createResponse.header.messageId, diskEntry) + fileIdDiskEntryMap.put(diskEntry.fileId, diskEntry) + } + } + + @Override + void handleOplockBreakNotification(OplockBreakNotification oplockBreakNotification) { + def oplockBreakLevel = oplockBreakNotification.oplockLevel + def getDiskEntry = fileIdDiskEntryMap.get(oplockBreakNotification.fileId) + if(getDiskEntry == null) { + throw new IllegalStateException("Unable to get corresponding diskEntry!") + } + // Assume we already notify client and had succeed handled client cache to break + if(oplockBreakLevel) { + // In this test case, this code should only run exactly once. + succeedBreakToLevel2.compareAndSet(false, true) + } + // Should return to client for handling the client cache, dummy in test case + def oplockBreakAcknowledgmentResponse = getDiskEntry.acknowledgeOplockBreak(oplockBreakLevel) + if(oplockBreakAcknowledgmentResponse.header.status == NtStatus.STATUS_SUCCESS) { + // In this test case, this code should only run exactly once. + oplockBreakAcknowledgmentResponseSucceed.compareAndSet(false, true) + } + } + }) + + when: + def firstCreateMessageId = 0L + share.openAsync(path, SMB2OplockLevel.SMB2_OPLOCK_LEVEL_EXCLUSIVE, null, EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_OPEN_IF, null, new MessageIdCallback() { + + @Override + void callback(long messageId) { + messageIdPathMap.put(messageId, path) + firstCreateMessageId = messageId + } + }) + + then: + // 1 second should be enough for the whole process complete in docker + Thread.sleep(1000L) + def firstCreateDiskEntry = messageIdDiskEntryMap.remove(firstCreateMessageId) + // another create to the same file with SMB2_OPLOCK_LEVEL_EXCLUSIVE to trigger oplock break notification in Server. + def secondCreateMessageId = 0L + share.openAsync(path, SMB2OplockLevel.SMB2_OPLOCK_LEVEL_EXCLUSIVE, null, EnumSet.of(AccessMask.GENERIC_READ, AccessMask.GENERIC_WRITE), null, SMB2ShareAccess.ALL, FILE_OPEN_IF, null, new MessageIdCallback() { + + @Override + void callback(long messageId) { + messageIdPathMap.put(messageId, path) + secondCreateMessageId = messageId + } + }) + // 1 second should be enough for the whole process complete in docker + Thread.sleep(1000L) + def secondCreateDiskEntry = messageIdDiskEntryMap.remove(secondCreateMessageId) + + expect: + firstCreateDiskEntry != null + secondCreateDiskEntry != null + succeedBreakToLevel2.get() == true + oplockBreakAcknowledgmentResponseSucceed.get() == true + + cleanup: + share.rm(path) + messageIdPathMap.clear() + messageIdDiskEntryMap.clear() + fileIdDiskEntryMap.clear() + + } } diff --git a/src/main/java/com/hierynomus/mserref/NtStatus.java b/src/main/java/com/hierynomus/mserref/NtStatus.java index fd769328..50843495 100644 --- a/src/main/java/com/hierynomus/mserref/NtStatus.java +++ b/src/main/java/com/hierynomus/mserref/NtStatus.java @@ -65,6 +65,7 @@ public enum NtStatus implements EnumWithValue { STATUS_NOT_SAME_DEVICE(0xC00000D4L), STATUS_FILE_RENAMED(0xC00000D5L), STATUS_OPLOCK_NOT_GRANTED(0xC00000E2L), + STATUS_INVALID_OPLOCK_PROTOCOL(0xC00000E3L), STATUS_INTERNAL_ERROR(0xC00000E5L), STATUS_UNEXPECTED_IO_ERROR(0xC00000E9L), STATUS_DIRECTORY_NOT_EMPTY(0xC0000101L), diff --git a/src/main/java/com/hierynomus/mssmb2/SMB2FileId.java b/src/main/java/com/hierynomus/mssmb2/SMB2FileId.java index fbd105a2..ed356c27 100644 --- a/src/main/java/com/hierynomus/mssmb2/SMB2FileId.java +++ b/src/main/java/com/hierynomus/mssmb2/SMB2FileId.java @@ -19,6 +19,8 @@ import com.hierynomus.protocol.commons.buffer.Buffer; import com.hierynomus.smb.SMBBuffer; +import java.util.Arrays; + /** * [MS-SMB2].pdf 2.2.14.1 SMB2_FILEID */ @@ -53,4 +55,24 @@ public String toString() { "persistentHandle=" + ByteArrayUtils.printHex(persistentHandle) + '}'; } + + public String toHexString() { + return ByteArrayUtils.toHex(persistentHandle) + ByteArrayUtils.toHex(volatileHandle); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SMB2FileId smb2FileId = (SMB2FileId) o; + return Arrays.equals(persistentHandle, smb2FileId.persistentHandle) && + Arrays.equals(volatileHandle, smb2FileId.volatileHandle); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(persistentHandle); + result = 31 * result + Arrays.hashCode(volatileHandle); + return result; + } } diff --git a/src/main/java/com/hierynomus/mssmb2/SMB2OplockLevel.java b/src/main/java/com/hierynomus/mssmb2/SMB2OplockLevel.java new file mode 100644 index 00000000..28887ebf --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/SMB2OplockLevel.java @@ -0,0 +1,42 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2; + +import com.hierynomus.protocol.commons.EnumWithValue; + +/** + * [MS-SMB2].pdf 2.2.13 SMB2 CREATE Request - OplockLevel + *

+ */ +public enum SMB2OplockLevel implements EnumWithValue { + SMB2_OPLOCK_LEVEL_NONE(0x00L), + SMB2_OPLOCK_LEVEL_II(0x01L), + SMB2_OPLOCK_LEVEL_EXCLUSIVE(0x08L), + SMB2_OPLOCK_LEVEL_BATCH(0x09L), + // TODO implement and support using lease + OPLOCK_LEVEL_LEASE(0xFFL); + + private long value; + + SMB2OplockLevel(long value) { + this.value = value; + } + + @Override + public long getValue() { + return value; + } +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateRequest.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateRequest.java index f3170c11..f2544947 100644 --- a/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateRequest.java +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateRequest.java @@ -39,16 +39,19 @@ public class SMB2CreateRequest extends SMB2Packet { private final SmbPath path; private final Set accessMask; private final SMB2ImpersonationLevel impersonationLevel; + private final SMB2OplockLevel oplockLevel; @SuppressWarnings("PMD.ExcessiveParameterList") public SMB2CreateRequest(SMB2Dialect smbDialect, long sessionId, long treeId, + SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions, SmbPath path) { super(57, smbDialect, SMB2MessageCommandCode.SMB2_CREATE, sessionId, treeId); + this.oplockLevel = ensureNotNull(oplockLevel, SMB2OplockLevel.SMB2_OPLOCK_LEVEL_NONE); this.impersonationLevel = ensureNotNull(impersonationLevel, SMB2ImpersonationLevel.Identification); this.accessMask = accessMask; this.fileAttributes = ensureNotNull(fileAttributes, FileAttributes.class); @@ -62,7 +65,7 @@ public SMB2CreateRequest(SMB2Dialect smbDialect, protected void writeTo(SMBBuffer buffer) { buffer.putUInt16(structureSize); // StructureSize (2 bytes) buffer.putByte((byte) 0); // SecurityFlags (1 byte) - Reserved - buffer.putByte((byte) 0); // RequestedOpLockLevel (1 byte) - None + buffer.putByte((byte)oplockLevel.getValue()); // RequestedOpLockLevel (1 byte) buffer.putUInt32(impersonationLevel.getValue()); // ImpersonationLevel (4 bytes) - Identification buffer.putReserved(8); // SmbCreateFlags (8 bytes) buffer.putReserved(8); // Reserved (8 bytes) @@ -95,4 +98,8 @@ protected void writeTo(SMBBuffer buffer) { buffer.putRawBytes(nameBytes); } + + public SmbPath getPath() { + return path; + } } diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateResponse.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateResponse.java index 274f20b0..407e6914 100644 --- a/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateResponse.java +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2CreateResponse.java @@ -20,6 +20,7 @@ import com.hierynomus.msfscc.FileAttributes; import com.hierynomus.mssmb2.SMB2CreateAction; import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2OplockLevel; import com.hierynomus.mssmb2.SMB2Packet; import com.hierynomus.protocol.commons.EnumWithValue; import com.hierynomus.protocol.commons.buffer.Buffer; @@ -34,26 +35,29 @@ */ public class SMB2CreateResponse extends SMB2Packet { + private SMB2OplockLevel oplockLevel; private SMB2CreateAction createAction; private FileTime creationTime; private FileTime lastAccessTime; private FileTime lastWriteTime; private FileTime changeTime; + private long allocationSize; + private long endOfFile; private Set fileAttributes; private SMB2FileId fileId; @Override protected void readMessage(SMBBuffer buffer) throws Buffer.BufferException { buffer.readUInt16(); // StructureSize (2 bytes) - buffer.readByte(); // OpLockLevel (1 byte) - Not used yet + oplockLevel = EnumWithValue.EnumUtils.valueOf(buffer.readByte(), SMB2OplockLevel.class, SMB2OplockLevel.SMB2_OPLOCK_LEVEL_NONE); // OpLockLevel (1 byte) buffer.readByte(); // Flags (1 byte) - Only for 3.x else Reserved createAction = EnumWithValue.EnumUtils.valueOf(buffer.readUInt32(), SMB2CreateAction.class, null); // CreateAction (4 bytes) creationTime = MsDataTypes.readFileTime(buffer); // CreationTime (8 bytes) lastAccessTime = MsDataTypes.readFileTime(buffer); // LastAccessTime (8 bytes) lastWriteTime = MsDataTypes.readFileTime(buffer); // LastWriteTime (8 bytes) changeTime = MsDataTypes.readFileTime(buffer); // ChangeTime (8 bytes) - buffer.readRawBytes(8); // AllocationSize (8 bytes) - Ignore - buffer.readRawBytes(8); // EndOfFile (8 bytes) + allocationSize = buffer.readLong(); // AllocationSize (8 bytes) + endOfFile = buffer.readUInt64(); // EndOfFile (8 bytes) fileAttributes = toEnumSet(buffer.readUInt32(), FileAttributes.class); // FileAttributes (4 bytes) buffer.skip(4); // Reserved2 (4 bytes) fileId = SMB2FileId.read(buffer); // FileId (16 bytes) @@ -63,6 +67,10 @@ protected void readMessage(SMBBuffer buffer) throws Buffer.BufferException { buffer.readUInt32();// CreateContextsLength (4 bytes) } + public SMB2OplockLevel getOplockLevel() { + return oplockLevel; + } + public SMB2CreateAction getCreateAction() { return createAction; } @@ -83,6 +91,14 @@ public FileTime getChangeTime() { return changeTime; } + public long getAllocationSize() { + return allocationSize; + } + + public long getEndOfFile() { + return endOfFile; + } + public Set getFileAttributes() { return fileAttributes; } diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2MessageConverter.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2MessageConverter.java index 42a0abd7..3d7768c8 100644 --- a/src/main/java/com/hierynomus/mssmb2/messages/SMB2MessageConverter.java +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2MessageConverter.java @@ -25,6 +25,8 @@ public class SMB2MessageConverter implements PacketFactory { + private SMB2OplockBreakFactory oplockBreakFactory = new SMB2OplockBreakFactory(); + private SMB2Packet read(SMBBuffer buffer) throws Buffer.BufferException { // Check we see a valid header start Check.ensureEquals(buffer.readRawBytes(4), new byte[]{(byte) 0xFE, 'S', 'M', 'B'}, "Could not find SMB2 Packet header"); @@ -66,9 +68,10 @@ private SMB2Packet read(SMBBuffer buffer) throws Buffer.BufferException { return read(new SMB2QueryInfoResponse(), buffer); case SMB2_SET_INFO: return read(new SMB2SetInfoResponse(), buffer); + case SMB2_OPLOCK_BREAK: + return oplockBreakFactory.read(buffer); case SMB2_LOCK: case SMB2_CANCEL: - case SMB2_OPLOCK_BREAK: default: throw new SMBRuntimeException("Unknown SMB2 Message Command type: " + command); diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreak.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreak.java new file mode 100644 index 00000000..21f09c34 --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreak.java @@ -0,0 +1,49 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +import com.hierynomus.mssmb2.SMB2Dialect; +import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2MessageCommandCode; +import com.hierynomus.mssmb2.SMB2OplockLevel; +import com.hierynomus.mssmb2.SMB2Packet; + +public abstract class SMB2OplockBreak extends SMB2Packet { + + protected SMB2OplockLevel oplockLevel; + protected SMB2FileId fileId; + + protected SMB2OplockBreak() { + super(); + } + + protected SMB2OplockBreak(int structureSize, SMB2Dialect dialect, long sessionId) { + super(structureSize, dialect, SMB2MessageCommandCode.SMB2_OPLOCK_BREAK, sessionId); + } + + protected SMB2OplockBreak(int structureSize, SMB2Dialect dialect, long sessionId, long treeId) { + super(structureSize, dialect, SMB2MessageCommandCode.SMB2_OPLOCK_BREAK, sessionId, treeId); + } + + public SMB2OplockLevel getOplockLevel() { + return oplockLevel; + } + + public SMB2FileId getFileId() { + return fileId; + } + +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgment.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgment.java new file mode 100644 index 00000000..8a649adc --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgment.java @@ -0,0 +1,42 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +import com.hierynomus.mssmb2.SMB2Dialect; +import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2OplockLevel; +import com.hierynomus.smb.SMBBuffer; + +/** + * [MS-SMB2].pdf 2.2.24 SMB2 OPLOCK_BREAK Acknowledgment + */ +public class SMB2OplockBreakAcknowledgment extends SMB2OplockBreak { + + public SMB2OplockBreakAcknowledgment(SMB2Dialect negotiatedDialect, long sessionId, long treeId, SMB2OplockLevel oplockLevel, SMB2FileId fileId) { + super(24, negotiatedDialect, sessionId, treeId); + this.oplockLevel = oplockLevel; + this.fileId = fileId; + } + + @Override + protected void writeTo(SMBBuffer buffer) { + buffer.putUInt16(structureSize); // StructureSize (2 bytes) + buffer.putByte((byte)oplockLevel.getValue()); // OpLockLevel (1 byte) + buffer.putReserved1(); // Reserved (1 bytes) + buffer.putReserved4(); // Reserved (4 bytes) + fileId.write(buffer); // FileId (16 bytes) + } +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgmentResponse.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgmentResponse.java new file mode 100644 index 00000000..b83181ea --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakAcknowledgmentResponse.java @@ -0,0 +1,23 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +/*** + * [MS-SMB2].pdf 2.2.25 SMB2 OPLOCK_BREAK Response + */ +public class SMB2OplockBreakAcknowledgmentResponse extends SMB2OplockBreakServerResponse { + +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakFactory.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakFactory.java new file mode 100644 index 00000000..87629a31 --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +import com.hierynomus.protocol.commons.buffer.Buffer; +import com.hierynomus.smb.SMBBuffer; + +public class SMB2OplockBreakFactory { + + // 3.2.5.19 Receiving an SMB2 OPLOCK_BREAK Notification + // If the MessageId field of the SMB2 header of the response is 0xFFFFFFFFFFFFFFFF, + // this MUST be processed as an oplock break indication. + // 0xFFFFFFFFFFFFFFFF == -1 + private static final long breakMessageId = -1; + + public SMB2OplockBreak read(SMBBuffer buffer) throws Buffer.BufferException { + + buffer.skip(24); + long messageId = buffer.readLong(); + buffer.rpos(0); + final boolean isBreakNotification = messageId == breakMessageId; + + // TODO Use structureSize as well to determine oplock and lease. + // buffer.skip(64); + // final int structureSize = buffer.readUInt16(); + // buffer.rpos(0); + + if (isBreakNotification) { + return read(new SMB2OplockBreakNotification(), buffer); + } else { + return read(new SMB2OplockBreakAcknowledgmentResponse(), buffer); + } + } + + private SMB2OplockBreak read(SMB2OplockBreak packet, SMBBuffer buffer) + throws Buffer.BufferException { + packet.read(buffer); + return packet; + } + +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakNotification.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakNotification.java new file mode 100644 index 00000000..2b89f60d --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakNotification.java @@ -0,0 +1,23 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +/** + * [MS-SMB2].pdf 2.2.23 SMB2 OPLOCK_BREAK Notification + */ +public class SMB2OplockBreakNotification extends SMB2OplockBreakServerResponse { + +} diff --git a/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakServerResponse.java b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakServerResponse.java new file mode 100644 index 00000000..e60f611a --- /dev/null +++ b/src/main/java/com/hierynomus/mssmb2/messages/SMB2OplockBreakServerResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.mssmb2.messages; + +import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2OplockLevel; +import com.hierynomus.protocol.commons.EnumWithValue; +import com.hierynomus.protocol.commons.buffer.Buffer; +import com.hierynomus.smb.SMBBuffer; + +public class SMB2OplockBreakServerResponse extends SMB2OplockBreak { + @Override + protected void readMessage(SMBBuffer buffer) throws Buffer.BufferException { + buffer.readUInt16(); // StructureSize (2 bytes) + oplockLevel = EnumWithValue.EnumUtils.valueOf(buffer.readByte(), SMB2OplockLevel.class, SMB2OplockLevel.SMB2_OPLOCK_LEVEL_NONE); // OpLockLevel (1 byte) + buffer.readByte(); // Reserved (1 byte) + buffer.skip(4); // Reserved2 (4 bytes) + fileId = SMB2FileId.read(buffer); // FileId (16 bytes) + } +} diff --git a/src/main/java/com/hierynomus/protocol/commons/concurrent/SingleThreadExecutorTaskQueue.java b/src/main/java/com/hierynomus/protocol/commons/concurrent/SingleThreadExecutorTaskQueue.java new file mode 100644 index 00000000..2eb0d942 --- /dev/null +++ b/src/main/java/com/hierynomus/protocol/commons/concurrent/SingleThreadExecutorTaskQueue.java @@ -0,0 +1,33 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.protocol.commons.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SingleThreadExecutorTaskQueue implements TaskQueue{ + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + @Override + public void execute(Runnable task) { + executorService.execute(task); + } + + public void close() { + executorService.shutdown(); + } +} diff --git a/src/main/java/com/hierynomus/protocol/commons/concurrent/TaskQueue.java b/src/main/java/com/hierynomus/protocol/commons/concurrent/TaskQueue.java new file mode 100644 index 00000000..84446c03 --- /dev/null +++ b/src/main/java/com/hierynomus/protocol/commons/concurrent/TaskQueue.java @@ -0,0 +1,29 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.protocol.commons.concurrent; + +/*** + * An object that executes submitted tasks in sequence. The TaskQueue MUST guarantee all + * submitted tasks will execute in sequence. + */ +public interface TaskQueue { + + /*** + * execute the task. The implementation MUST guarantee the execution is in sequence. + * @param task runnable task + */ + void execute(Runnable task); +} diff --git a/src/main/java/com/hierynomus/smbj/SMBClient.java b/src/main/java/com/hierynomus/smbj/SMBClient.java index 1f34d830..e29f18f8 100644 --- a/src/main/java/com/hierynomus/smbj/SMBClient.java +++ b/src/main/java/com/hierynomus/smbj/SMBClient.java @@ -45,7 +45,7 @@ public class SMBClient implements Closeable { private SmbConfig config; - private SMBEventBus bus; + private SMBEventBus clientGlobalBus; private PathResolver pathResolver; public SMBClient() { @@ -56,10 +56,10 @@ public SMBClient(SmbConfig config) { this(config, new SMBEventBus()); } - public SMBClient(SmbConfig config, SMBEventBus bus) { + public SMBClient(SmbConfig config, SMBEventBus clientGlobalBus) { this.config = config; - this.bus = bus; - bus.subscribe(this); + this.clientGlobalBus = clientGlobalBus; + clientGlobalBus.subscribe(this); this.pathResolver = new SymlinkPathResolver(PathResolver.LOCAL); if (config.isDfsEnabled()) { this.pathResolver = new DFSPathResolver(this.pathResolver); @@ -98,7 +98,7 @@ private Connection getEstablishedOrConnect(String hostname, int port) throws IOE String hostPort = hostname + ":" + port; Connection cachedConnection = connectionTable.get(hostPort); if (cachedConnection == null || !cachedConnection.isConnected()) { - Connection connection = new Connection(config, this, bus); + Connection connection = new Connection(config, this, clientGlobalBus); try { connection.connect(hostname, port); } catch (IOException e) { diff --git a/src/main/java/com/hierynomus/smbj/SmbConfig.java b/src/main/java/com/hierynomus/smbj/SmbConfig.java index 3ba01127..e395ce14 100644 --- a/src/main/java/com/hierynomus/smbj/SmbConfig.java +++ b/src/main/java/com/hierynomus/smbj/SmbConfig.java @@ -17,6 +17,7 @@ import com.hierynomus.mssmb2.SMB2Dialect; import com.hierynomus.protocol.commons.Factory; +import com.hierynomus.protocol.commons.concurrent.TaskQueue; import com.hierynomus.protocol.commons.socket.ProxySocketFactory; import com.hierynomus.security.SecurityProvider; import com.hierynomus.security.bc.BCSecurityProvider; @@ -72,6 +73,7 @@ public final class SmbConfig { private int transactBufferSize; private TransportLayerFactory> transportLayerFactory; private long transactTimeout; + private TaskQueue taskQueue; private int soTimeout; @@ -94,7 +96,9 @@ public static Builder builder() { .withDialects(SMB2Dialect.SMB_2_1, SMB2Dialect.SMB_2_0_2) // order is important. The authenticators listed first will be selected .withAuthenticators(getDefaultAuthenticators()) - .withTimeout(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_UNIT); + .withTimeout(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_UNIT) + .withTaskQueue(null); + } private static SecurityProvider getDefaultSecurityProvider() { @@ -145,6 +149,7 @@ private SmbConfig(SmbConfig other) { transportLayerFactory = other.transportLayerFactory; soTimeout = other.soTimeout; useMultiProtocolNegotiate = other.useMultiProtocolNegotiate; + taskQueue = other.taskQueue; } public Random getRandomProvider() { @@ -219,6 +224,10 @@ public SocketFactory getSocketFactory() { return socketFactory; } + public TaskQueue getTaskQueue() { + return taskQueue; + } + public static class Builder { private SmbConfig config; @@ -397,5 +406,10 @@ public Builder withMultiProtocolNegotiate(boolean useMultiProtocolNegotiate) { config.useMultiProtocolNegotiate = useMultiProtocolNegotiate; return this; } + + public Builder withTaskQueue(TaskQueue taskQueue) { + config.taskQueue = taskQueue; + return this; + } } } diff --git a/src/main/java/com/hierynomus/smbj/connection/Connection.java b/src/main/java/com/hierynomus/smbj/connection/Connection.java index 9ac109c2..fe78dfe3 100644 --- a/src/main/java/com/hierynomus/smbj/connection/Connection.java +++ b/src/main/java/com/hierynomus/smbj/connection/Connection.java @@ -34,10 +34,16 @@ import com.hierynomus.smbj.auth.AuthenticationContext; import com.hierynomus.smbj.auth.Authenticator; import com.hierynomus.smbj.common.SMBRuntimeException; +import com.hierynomus.smbj.event.AsyncCreateRequestNotification; +import com.hierynomus.smbj.event.AsyncCreateResponseNotification; +import com.hierynomus.smbj.event.AsyncRequestMessageIdNotification; import com.hierynomus.smbj.event.ConnectionClosed; +import com.hierynomus.smbj.event.OplockBreakNotification; import com.hierynomus.smbj.event.SMBEventBus; import com.hierynomus.smbj.event.SessionLoggedOff; +import com.hierynomus.smbj.event.handler.MessageIdCallback; import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; import com.hierynomus.spnego.NegTokenInit; import com.hierynomus.spnego.SpnegoException; import net.engio.mbassy.listener.Handler; @@ -83,24 +89,25 @@ public SMBClient getClient() { private SmbConfig config; private TransportLayer> transport; - private final SMBEventBus bus; + private final SMBEventBus clientGlobalBus; + private final SMBEventBus connectionPrivateBus = new SMBEventBus(); private final ReentrantLock lock = new ReentrantLock(); private int remotePort; - public Connection(SmbConfig config, SMBClient client, SMBEventBus bus) { + public Connection(SmbConfig config, SMBClient client, SMBEventBus clientGlobalBus) { this.config = config; this.client = client; this.transport = config.getTransportLayerFactory().createTransportLayer(new PacketHandlers<>(new SMBPacketSerializer(), this, converter), config); - this.bus = bus; - bus.subscribe(this); + this.clientGlobalBus = clientGlobalBus; + connectionPrivateBus.subscribe(this); } public Connection(Connection connection) { this.client = connection.client; this.config = connection.config; this.transport = connection.transport; - this.bus = connection.bus; - bus.subscribe(this); + this.clientGlobalBus = connection.clientGlobalBus; + connectionPrivateBus.subscribe(this); } public void connect(String hostname, int port) throws IOException { @@ -142,7 +149,7 @@ public void close(boolean force) throws IOException { } finally { transport.disconnect(); logger.info("Closed connection to {}", getRemoteHostname()); - bus.publish(new ConnectionClosed(remoteName, remotePort)); + clientGlobalBus.publish(new ConnectionClosed(remoteName, remotePort)); } } @@ -200,7 +207,7 @@ public Session authenticate(AuthenticationContext authContext) { } private Session getSession(AuthenticationContext authContext) { - return new Session(this, authContext, bus, client.getPathResolver(), config.getSecurityProvider()); + return new Session(this, authContext, connectionPrivateBus, client.getPathResolver(), config.getSecurityProvider()); } private byte[] processAuthenticationToken(Authenticator authenticator, AuthenticationContext authContext, byte[] inputToken, Session session) throws IOException { @@ -255,6 +262,19 @@ private Authenticator getAuthenticator(AuthenticationContext context) throws IOE * @throws TransportException When a transport level error occurred */ public Future send(SMB2Packet packet) throws TransportException { + return send(packet, null); + } + + /*** + * Send a packet and callback for the corresponding messageId. Currently, only support SMB2CreateRequest + * + * @param packet SMBPacket to send + * @param messageIdCallback callback to return corresponding messageId + * @return a Future to be used to retrieve the response packet + * @throws TransportException When a transport level error occurred + */ + public Future send(SMB2Packet packet, MessageIdCallback messageIdCallback) throws TransportException { + lock.lock(); try { int availableCredits = sequenceWindow.available(); @@ -267,6 +287,28 @@ public Future send(SMB2Packet packet) throws Transport logger.debug("Granted {} (out of {}) credits to {}", grantCredits, availableCredits, packet); packet.getHeader().setCreditRequest(Math.max(SequenceWindow.PREFERRED_MINIMUM_CREDITS - availableCredits - grantCredits, grantCredits)); + long messageId = packet.getHeader().getMessageId(); + + if (messageIdCallback != null) { + messageIdCallback.callback(messageId); + } + + if (DiskShare.asyncSupport.contains(packet.getHeader().getMessage())) { + connectionPrivateBus.publish(new AsyncRequestMessageIdNotification( + packet.getHeader().getSessionId(), + packet.getHeader().getTreeId(), + messageId) + ); + } + + if (packet.getHeader().getMessage() == SMB2MessageCommandCode.SMB2_CREATE) { + connectionPrivateBus.publish(new AsyncCreateRequestNotification( + packet.getHeader().getSessionId(), + packet.getHeader().getTreeId(), + messageId) + ); + } + Request request = new Request(packet.getHeader().getMessageId(), UUID.randomUUID()); outstandingRequests.registerOutstanding(request); transport.write(packet); @@ -370,6 +412,21 @@ public void handle(SMBPacket uncheckedPacket) throws TransportException { long messageId = packet.getSequenceNumber(); if (!outstandingRequests.isOutstanding(messageId)) { + + // 3.2.5.19 Receiving an SMB2 OPLOCK_BREAK Notification + // If the MessageId field of the SMB2 header of the response is 0xFFFFFFFFFFFFFFFF, + // this MUST be + // processed as an oplock break indication. + if (packet instanceof SMB2OplockBreakNotification) { + SMB2OplockBreakNotification oplockBreakNotification = (SMB2OplockBreakNotification)packet; + logger.debug("Received SMB2OplockBreakNotification Packet for FileId {} with {}", oplockBreakNotification.getFileId(), oplockBreakNotification.getOplockLevel()); + connectionPrivateBus.publish(new OplockBreakNotification( + oplockBreakNotification.getOplockLevel(), + oplockBreakNotification.getFileId() + )); + return; + } + throw new TransportException("Received response with unknown sequence number <<" + messageId + ">>"); } @@ -409,6 +466,17 @@ public void handle(SMBPacket uncheckedPacket) throws TransportException { verifyPacketSignature(packet, session); } + // Handing case for Oplock/Lease related issue + if (packet instanceof SMB2CreateResponse) { + SMB2CreateResponse smb2CreateResponse = (SMB2CreateResponse)packet; + connectionPrivateBus.publish(new AsyncCreateResponseNotification( + messageId, + smb2CreateResponse.getFileId(), + smb2CreateResponse) + ); + + } + // [MS-SMB2].pdf 3.2.5.1.8 Processing the Response outstandingRequests.receivedResponseFor(messageId).getPromise().deliver(packet); } diff --git a/src/main/java/com/hierynomus/smbj/event/AbstractAsyncRequestNotification.java b/src/main/java/com/hierynomus/smbj/event/AbstractAsyncRequestNotification.java new file mode 100644 index 00000000..7dbd0655 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AbstractAsyncRequestNotification.java @@ -0,0 +1,37 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +public abstract class AbstractAsyncRequestNotification implements AsyncRequestNotification { + + private long sessionId; + private long treeId; + + public AbstractAsyncRequestNotification(long sessionId, long treeId) { + this.sessionId = sessionId; + this.treeId = treeId; + } + + @Override + public long getSessionId() { + return sessionId; + } + + @Override + public long getTreeId() { + return treeId; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/AbstractAsyncResponseNotification.java b/src/main/java/com/hierynomus/smbj/event/AbstractAsyncResponseNotification.java new file mode 100644 index 00000000..ef714f9a --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AbstractAsyncResponseNotification.java @@ -0,0 +1,30 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +public abstract class AbstractAsyncResponseNotification implements AsyncResponseNotification { + + private long messageId; + + public AbstractAsyncResponseNotification(long messageId) { + this.messageId = messageId; + } + + @Override + public long getMessageId() { + return messageId; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncCreateRequestNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncCreateRequestNotification.java new file mode 100644 index 00000000..f989ae6a --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncCreateRequestNotification.java @@ -0,0 +1,34 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +/*** + * Event for notifying the messageId to DiskShare Notification Handler + */ +public class AsyncCreateRequestNotification extends AbstractAsyncRequestNotification + implements SMBEvent { + + private long messageId; + + public AsyncCreateRequestNotification(long sessionId, long treeId, long messageId) { + super(sessionId, treeId); + this.messageId = messageId; + } + + public long getMessageId() { + return messageId; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncCreateResponseNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncCreateResponseNotification.java new file mode 100644 index 00000000..34dbd820 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncCreateResponseNotification.java @@ -0,0 +1,44 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.messages.SMB2CreateResponse; + +/*** + * Event for notifying the fileId and CreateResponseFuture to corresponding messageId on AysncCreate + */ +public class AsyncCreateResponseNotification extends AbstractAsyncResponseNotification + implements SMBEvent { + + private SMB2FileId fileId; + private SMB2CreateResponse createResponse; + + public AsyncCreateResponseNotification(long messageId, SMB2FileId fileId, + SMB2CreateResponse createResponse) { + super(messageId); + this.fileId = fileId; + this.createResponse = createResponse; + } + + public SMB2FileId getFileId() { + return fileId; + } + + public SMB2CreateResponse getCreateResponse() { + return createResponse; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncNotification.java new file mode 100644 index 00000000..384e2ad6 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncNotification.java @@ -0,0 +1,24 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +/** + * Base class for asynchronous notification events that need to be handled by notification + * handlers (observers) + */ +public interface AsyncNotification { + +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncRequestMessageIdNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncRequestMessageIdNotification.java new file mode 100644 index 00000000..1f1630cc --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncRequestMessageIdNotification.java @@ -0,0 +1,41 @@ + /* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +public class AsyncRequestMessageIdNotification implements SMBEvent { + + private long sessionId; + private long treeId; + private long messageId; + + public AsyncRequestMessageIdNotification(long sessionId, long treeId, long messageId) { + this.sessionId = sessionId; + this.treeId = treeId; + this.messageId = messageId; + } + + public long getSessionId() { + return sessionId; + } + + public long getTreeId() { + return treeId; + } + + public long getMessageId() { + return messageId; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncRequestNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncRequestNotification.java new file mode 100644 index 00000000..c8484fc6 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncRequestNotification.java @@ -0,0 +1,23 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +public interface AsyncRequestNotification extends AsyncNotification { + + long getSessionId(); + + long getTreeId(); +} diff --git a/src/main/java/com/hierynomus/smbj/event/AsyncResponseNotification.java b/src/main/java/com/hierynomus/smbj/event/AsyncResponseNotification.java new file mode 100644 index 00000000..227f6ebf --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/AsyncResponseNotification.java @@ -0,0 +1,21 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +public interface AsyncResponseNotification extends AsyncNotification { + + long getMessageId(); +} diff --git a/src/main/java/com/hierynomus/smbj/event/OplockBreakNotification.java b/src/main/java/com/hierynomus/smbj/event/OplockBreakNotification.java new file mode 100644 index 00000000..578f535b --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/OplockBreakNotification.java @@ -0,0 +1,41 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event; + +import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2OplockLevel; + +/*** + * Event for notifying the oplock break notification for corresponding fileId + */ +public class OplockBreakNotification implements SMBEvent, AsyncNotification { + + private SMB2OplockLevel oplockLevel; + private SMB2FileId fileId; + + public OplockBreakNotification(SMB2OplockLevel oplockLevel, SMB2FileId fileId) { + this.oplockLevel = oplockLevel; + this.fileId = fileId; + } + + public SMB2OplockLevel getOplockLevel() { + return oplockLevel; + } + + public SMB2FileId getFileId() { + return fileId; + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/handler/AbstractNotificationHandler.java b/src/main/java/com/hierynomus/smbj/event/handler/AbstractNotificationHandler.java new file mode 100644 index 00000000..2d323c91 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/handler/AbstractNotificationHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event.handler; + +import com.hierynomus.smbj.event.AsyncCreateRequestNotification; +import com.hierynomus.smbj.event.AsyncCreateResponseNotification; +import com.hierynomus.smbj.event.OplockBreakNotification; + +/*** + * Abstract class for user only interested in some notification to override + */ +public abstract class AbstractNotificationHandler implements NotificationHandler { + + @Override + public void handleAsyncCreateRequestNotification( + AsyncCreateRequestNotification asyncCreateRequestNotification) { + // Empty method for override + // This is useful for user only implement some methods of NotificationHandler + // Leave this method Empty will perform nothing when received an asyncCreateRequestNotification + } + + @Override + public void handleAsyncCreateResponseNotification( + AsyncCreateResponseNotification asyncCreateResponseNotification) { + // Empty method for override + // This is useful for user only implement some methods of NotificationHandler + // Leave this method Empty will perform nothing when received an asyncCreateResponseNotification + } + + @Override + public void handleOplockBreakNotification(OplockBreakNotification oplockBreakNotification) { + // Empty method for override + // This is useful for user only implement some methods of NotificationHandler + // Leave this method Empty will perform nothing when received an oplockBreakNotification + } +} diff --git a/src/main/java/com/hierynomus/smbj/event/handler/MessageIdCallback.java b/src/main/java/com/hierynomus/smbj/event/handler/MessageIdCallback.java new file mode 100644 index 00000000..c227e543 --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/handler/MessageIdCallback.java @@ -0,0 +1,20 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event.handler; + +public interface MessageIdCallback { + void callback(long messageId); +} diff --git a/src/main/java/com/hierynomus/smbj/event/handler/NotificationHandler.java b/src/main/java/com/hierynomus/smbj/event/handler/NotificationHandler.java new file mode 100644 index 00000000..171d483c --- /dev/null +++ b/src/main/java/com/hierynomus/smbj/event/handler/NotificationHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (C)2016 - SMBJ Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hierynomus.smbj.event.handler; + +import com.hierynomus.smbj.event.AsyncCreateRequestNotification; +import com.hierynomus.smbj.event.AsyncCreateResponseNotification; +import com.hierynomus.smbj.event.OplockBreakNotification; + +public interface NotificationHandler { + // TODO add user reference property to allow user to check same NotificationHandler or not. + void handleAsyncCreateRequestNotification(AsyncCreateRequestNotification asyncCreateRequestNotification); + void handleAsyncCreateResponseNotification(AsyncCreateResponseNotification asyncCreateResponseNotification); + void handleOplockBreakNotification(OplockBreakNotification oplockBreakNotification); +} diff --git a/src/main/java/com/hierynomus/smbj/session/Session.java b/src/main/java/com/hierynomus/smbj/session/Session.java index e2cc033e..11422210 100644 --- a/src/main/java/com/hierynomus/smbj/session/Session.java +++ b/src/main/java/com/hierynomus/smbj/session/Session.java @@ -29,6 +29,7 @@ import com.hierynomus.smbj.event.SMBEventBus; import com.hierynomus.smbj.event.SessionLoggedOff; import com.hierynomus.smbj.event.TreeDisconnected; +import com.hierynomus.smbj.event.handler.MessageIdCallback; import com.hierynomus.smbj.paths.PathResolveException; import com.hierynomus.smbj.paths.PathResolver; import com.hierynomus.smbj.share.*; @@ -56,7 +57,7 @@ public class Session implements AutoCloseable { private boolean encryptData; // SMB3.x private Connection connection; - private SMBEventBus bus; + private final SMBEventBus connectionPrivateBus; private final PathResolver pathResolver; private TreeConnectTable treeConnectTable = new TreeConnectTable(); private List nestedSessions = new ArrayList<>(); @@ -65,14 +66,14 @@ public class Session implements AutoCloseable { private boolean guest; private boolean anonymous; - public Session(Connection connection, AuthenticationContext userCredentials, SMBEventBus bus, PathResolver pathResolver, SecurityProvider securityProvider) { + public Session(Connection connection, AuthenticationContext userCredentials, SMBEventBus connectionPrivateBus, PathResolver pathResolver, SecurityProvider securityProvider) { this.connection = connection; this.userCredentials = userCredentials; - this.bus = bus; + this.connectionPrivateBus = connectionPrivateBus; this.pathResolver = pathResolver; this.packetSignatory = new PacketSignatory(connection.getNegotiatedProtocol().getDialect(), securityProvider); - if (bus != null) { - bus.subscribe(this); + if (connectionPrivateBus != null) { + connectionPrivateBus.subscribe(this); } } @@ -176,11 +177,11 @@ private Share connectTree(final String shareName) { } long treeId = response.getHeader().getTreeId(); - TreeConnect treeConnect = new TreeConnect(treeId, smbPath, this, response.getCapabilities(), connection, bus, response.getMaximalAccess()); + TreeConnect treeConnect = new TreeConnect(treeId, smbPath, this, response.getCapabilities(), connection, connectionPrivateBus, response.getMaximalAccess()); Share share; if (response.isDiskShare()) { - share = new DiskShare(smbPath, treeConnect, pathResolver); + share = new DiskShare(smbPath, treeConnect, pathResolver, connectionPrivateBus); } else if (response.isNamedPipe()) { share = new PipeShare(smbPath, treeConnect); } else if (response.isPrinterShare()) { @@ -240,7 +241,7 @@ public void logoff() throws TransportException { throw new SMBApiException(response.getHeader(), "Could not logoff session <<" + sessionId + ">>"); } } finally { - bus.publish(new SessionLoggedOff(sessionId)); + connectionPrivateBus.publish(new SessionLoggedOff(sessionId)); } } @@ -277,10 +278,22 @@ public Connection getConnection() { * @throws TransportException */ public Future send(SMB2Packet packet) throws TransportException { + return send(packet, null); + } + + /*** + * send a packet and callback for the corresponding messageId. The packet will be signed or not depending on the session's flags. + * + * @param packet SMBPacket to send + * @param messageIdCallback callback to return corresponding messageId + * @return a Future to be used to retrieve the response packet + * @throws TransportException + */ + public Future send(SMB2Packet packet, MessageIdCallback messageIdCallback) throws TransportException { if (signingRequired && !packetSignatory.isInitialized()) { throw new TransportException("Message signing is required, but no signing key is negotiated"); } - return connection.send(packetSignatory.sign(packet)); + return connection.send(packetSignatory.sign(packet), messageIdCallback); } public T processSendResponse(SMB2CreateRequest packet) throws TransportException { diff --git a/src/main/java/com/hierynomus/smbj/share/DiskEntry.java b/src/main/java/com/hierynomus/smbj/share/DiskEntry.java index 00948635..a198ec9c 100644 --- a/src/main/java/com/hierynomus/smbj/share/DiskEntry.java +++ b/src/main/java/com/hierynomus/smbj/share/DiskEntry.java @@ -23,7 +23,9 @@ import com.hierynomus.msfscc.fileinformation.FileRenameInformation; import com.hierynomus.msfscc.fileinformation.FileSettableInformation; import com.hierynomus.mssmb2.SMB2FileId; +import com.hierynomus.mssmb2.SMB2OplockLevel; import com.hierynomus.mssmb2.SMBApiException; +import com.hierynomus.mssmb2.messages.SMB2OplockBreakAcknowledgmentResponse; import com.hierynomus.protocol.transport.TransportException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +51,14 @@ public void close() { share.closeFileId(fileId); } + public DiskShare getShare() { + return share; + } + + public String getFileName() { + return fileName; + } + public SMB2FileId getFileId() { return fileId; } @@ -181,4 +191,14 @@ public void closeSilently() { logger.warn("File close failed for {},{},{}", fileName, share, fileId, e); } } + + /*** + * Send a acknowledgment for Oplock Break Notification. 2.2.24 SMB2 OPLOCK_BREAK Acknowledgment. + * + * @param oplockLevel the oplock break level after receiving the oplock break notification (current holding oplock level) + * @return Server response to oplock break acknowledgment. 2.2.25 SMB2 OPLOCK_BREAK Response. + */ + public SMB2OplockBreakAcknowledgmentResponse acknowledgeOplockBreak(SMB2OplockLevel oplockLevel) { + return share.sendOplockBreakAcknowledgment(fileId, oplockLevel); + } } diff --git a/src/main/java/com/hierynomus/smbj/share/DiskShare.java b/src/main/java/com/hierynomus/smbj/share/DiskShare.java index eee425ae..28378ce6 100644 --- a/src/main/java/com/hierynomus/smbj/share/DiskShare.java +++ b/src/main/java/com/hierynomus/smbj/share/DiskShare.java @@ -29,20 +29,36 @@ import com.hierynomus.protocol.commons.EnumWithValue; import com.hierynomus.protocol.commons.buffer.Buffer; import com.hierynomus.protocol.commons.buffer.Endian; +import com.hierynomus.protocol.commons.concurrent.SingleThreadExecutorTaskQueue; +import com.hierynomus.protocol.commons.concurrent.TaskQueue; import com.hierynomus.protocol.transport.TransportException; import com.hierynomus.smb.SMBBuffer; import com.hierynomus.smbj.SMBClient; import com.hierynomus.smbj.common.SMBRuntimeException; import com.hierynomus.smbj.common.SmbPath; -import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.event.AsyncCreateRequestNotification; +import com.hierynomus.smbj.event.AsyncCreateResponseNotification; +import com.hierynomus.smbj.event.AsyncRequestMessageIdNotification; +import com.hierynomus.smbj.event.OplockBreakNotification; +import com.hierynomus.smbj.event.SMBEventBus; +import com.hierynomus.smbj.event.handler.MessageIdCallback; +import com.hierynomus.smbj.event.handler.NotificationHandler; import com.hierynomus.smbj.paths.PathResolveException; import com.hierynomus.smbj.paths.PathResolver; import com.hierynomus.smbj.session.Session; +import net.engio.mbassy.listener.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; import static com.hierynomus.msdtyp.AccessMask.*; import static com.hierynomus.mserref.NtStatus.*; @@ -52,23 +68,114 @@ import static com.hierynomus.mssmb2.SMB2CreateDisposition.FILE_OPEN; import static com.hierynomus.mssmb2.SMB2CreateOptions.FILE_DIRECTORY_FILE; import static com.hierynomus.mssmb2.SMB2CreateOptions.FILE_NON_DIRECTORY_FILE; +import static com.hierynomus.mssmb2.SMB2MessageCommandCode.SMB2_CREATE; import static com.hierynomus.mssmb2.SMB2ShareAccess.*; import static com.hierynomus.mssmb2.messages.SMB2QueryInfoRequest.SMB2QueryInfoType.SMB2_0_INFO_SECURITY; import static java.util.EnumSet.of; import static java.util.EnumSet.noneOf; public class DiskShare extends Share { + public static final EnumSet asyncSupport = of(SMB2_CREATE); + private static final Logger logger = LoggerFactory.getLogger(DiskShare.class); private final PathResolver resolver; + private NotificationHandler notificationHandler = null; + + private final boolean isCreatedTaskQueue; + private final TaskQueue taskQueue; + private final Set openedOplockFileId = Collections.newSetFromMap(new ConcurrentHashMap()); + // Only add the messageId to Set when the operation is on the asyncSupport Set. Must remove when receive the corresponding AsyncResponse. + private final Set asyncOperationMessageId = Collections.newSetFromMap(new ConcurrentHashMap()); - public DiskShare(SmbPath smbPath, TreeConnect treeConnect, PathResolver pathResolver) { + public DiskShare(SmbPath smbPath, TreeConnect treeConnect, PathResolver pathResolver, SMBEventBus connectionPrivateBus) { super(smbPath, treeConnect); this.resolver = pathResolver; + if (connectionPrivateBus != null) { + connectionPrivateBus.subscribe(this); + } + TaskQueue taskQueueFromConfig = treeConnect.getConnection().getConfig().getTaskQueue(); + if (taskQueueFromConfig != null) { + taskQueue = taskQueueFromConfig; + isCreatedTaskQueue = false; + } else { + taskQueue = new SingleThreadExecutorTaskQueue(); + isCreatedTaskQueue = true; + } + } + + @Override + public void close() throws IOException { + super.close(); + if (isCreatedTaskQueue) { + // cleanup for executor + ((SingleThreadExecutorTaskQueue)taskQueue).close(); + } + // cleanup for set + openedOplockFileId.clear(); + asyncOperationMessageId.clear(); } public DiskEntry open(String path, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + return open(path, null, null, accessMask, attributes, shareAccesses, createDisposition, createOptions); + } + + public DiskEntry open(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + SMB2CreateResponseDiskEntry result = openWithResponse(path, oplockLevel, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions); + return result.getDiskEntry(); + } + + /*** + * Synchronously open a diskEntry. Returning the diskEntry with the createResponse. + * + * @param path target file path + * @param oplockLevel requesting oplock level + * @param impersonationLevel requesting impersonation level + * @param accessMask desired access + * @param attributes file attributes + * @param shareAccesses the share access of this create request + * @param createDisposition create disposition of this create request + * @param createOptions create options of this create request + * @return the diskEntry and the corresponding createResponse. + */ + public SMB2CreateResponseDiskEntry openWithResponse(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { SmbPath pathAndFile = new SmbPath(smbPath, path); - SMB2CreateResponseContext response = createFileAndResolve(pathAndFile, null, accessMask, attributes, shareAccesses, createDisposition, createOptions); - return getDiskEntry(path, response); + SMB2CreateResponseContext response = createFileAndResolve(pathAndFile, oplockLevel, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions); + return new SMB2CreateResponseDiskEntry(response.resp, getDiskEntry(path, response)); + } + + /*** + * Send a create request and return a Future for create response. User are required to deal with DFS issue by himself. + * + * @param path target file path + * @param oplockLevel requesting oplock level + * @param impersonationLevel requesting impersonation level + * @param accessMask desired access + * @param attributes file attributes + * @param shareAccesses the share access of this create request + * @param createDisposition create disposition of this create request + * @param createOptions create options of this create request + * @return a Future to be used to retrieve the create response packet + */ + public Future openAsync(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + return openAsync(path, oplockLevel, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions, null); + } + + /*** + * Send a create request and callback for messageId for create response. User are required to deal with DFS issue by himself. + * + * @param path target file path + * @param oplockLevel requesting oplock level + * @param impersonationLevel requesting impersonation level + * @param accessMask desired access + * @param attributes file attributes + * @param shareAccesses the share access of this create request + * @param createDisposition create disposition of this create request + * @param createOptions create options of this create request + * @param messageIdCallback callback to return corresponding messageId + * @return a Future to be used to retrieve the create response packet + */ + public Future openAsync(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions, MessageIdCallback messageIdCallback) { + SmbPath pathAndFile = new SmbPath(smbPath, path); + return super.createAsync(pathAndFile, oplockLevel, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions, messageIdCallback); } @Override @@ -76,8 +183,8 @@ protected Set getCreateSuccessStatus() { return resolver.handledStates(); } - private SMB2CreateResponseContext createFileAndResolve(SmbPath path, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { - SMB2CreateResponse resp = super.createFile(path, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); + private SMB2CreateResponseContext createFileAndResolve(SmbPath path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { + SMB2CreateResponse resp = super.createFile(path, oplockLevel, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); try { SmbPath target = resolver.resolve(session, resp, path); DiskShare resolveShare = this; @@ -89,7 +196,7 @@ private SMB2CreateResponseContext createFileAndResolve(SmbPath path, SMB2Imperso resolveShare = (DiskShare) connectedSession.connectShare(target.getShareName()); } if (!path.equals(target)) { - return resolveShare.createFileAndResolve(target, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); + return resolveShare.createFileAndResolve(target, oplockLevel, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); } } catch (PathResolveException e) { throw new SMBApiException(e.getStatus(), SMB2MessageCommandCode.SMB2_CREATE, "Cannot resolve path " + path, e); @@ -106,19 +213,32 @@ private Session buildNewSession(SMB2CreateResponse resp, SmbPath target) { } } - protected DiskEntry getDiskEntry(String path, SMB2CreateResponseContext responseContext) { + public DiskEntry getDiskEntry(String path, SMB2CreateResponseContext responseContext) { SMB2CreateResponse response = responseContext.resp; + DiskEntry diskEntry; if (response.getFileAttributes().contains(FILE_ATTRIBUTE_DIRECTORY)) { - return new Directory(response.getFileId(), responseContext.share, path); + diskEntry = new Directory(response.getFileId(), responseContext.share, path); } else { - return new File(response.getFileId(), responseContext.share, path); + diskEntry = new File(response.getFileId(), responseContext.share, path); + } + // if oplock level is not none, put it to set. + if (response.getOplockLevel() != SMB2OplockLevel.SMB2_OPLOCK_LEVEL_NONE) { + openedOplockFileId.add(diskEntry.fileId); } + return diskEntry; } /** * Get a handle to a directory in the given path */ public Directory openDirectory(String path, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + return openDirectory(path, null, null, accessMask, attributes, shareAccesses, createDisposition, createOptions); + } + + /** + * Get a handle to a directory in the given path + */ + public Directory openDirectory(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { EnumSet actualCreateOptions = createOptions != null ? EnumSet.copyOf(createOptions) : EnumSet.noneOf(SMB2CreateOptions.class); actualCreateOptions.add(FILE_DIRECTORY_FILE); actualCreateOptions.remove(FILE_NON_DIRECTORY_FILE); @@ -128,6 +248,8 @@ public Directory openDirectory(String path, Set accessMask, Set accessMask, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + return openFile(path, null, null, accessMask, attributes, shareAccesses, createDisposition, createOptions); + } + + /** + * Get a handle to a file in the given path + */ + public File openFile(String path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { EnumSet actualCreateOptions = createOptions != null ? EnumSet.copyOf(createOptions) : EnumSet.noneOf(SMB2CreateOptions.class); actualCreateOptions.add(FILE_NON_DIRECTORY_FILE); actualCreateOptions.remove(FILE_DIRECTORY_FILE); @@ -146,6 +278,8 @@ public File openFile(String path, Set accessMask, Set accessMask, Set security ); } + /*** + * 3.2.5.19 Receiving an SMB2 OPLOCK_BREAK Notification, this handler is responsible to call acknowledgeOplockBreak if needed. Set the handler for Receiving an Oplock Break Notification. + * You MUST set this handler before create/open diskEntry with oplock. + * + * @param handler handler for Receiving an Oplock Break Notification and Async Create Request/Response. + */ + public void setNotificationHandler(NotificationHandler handler) { + this.notificationHandler = handler; + } + + /*** + * Record the messageId for the Async Operation. + * + * @param asyncRequestMessageIdNotification messageId requires handle Async Response for specific sessionId and treeId. + */ + @Handler + @SuppressWarnings("unused") + private void setMessageIdForSupportedAsyncOperation(final AsyncRequestMessageIdNotification asyncRequestMessageIdNotification) { + try { + if (asyncRequestMessageIdNotification.getSessionId() == this.sessionId + && asyncRequestMessageIdNotification.getTreeId() == this.treeId) { + // add the messageId to Set if Async Request is sending by this DiskShare + asyncOperationMessageId.add(asyncRequestMessageIdNotification.getMessageId()); + } + } catch (Throwable t) { + logger.error("Handling setMessageIdForSupportedAsyncOperation error occur : ", t); + throw t; + } + } + + /*** + * Handler for handing the oplock break notification event from server. 3.2.5.19 Receiving an SMB2 OPLOCK_BREAK Notification. + * + * @param oplockBreakNotification received oplock break notification from server. + */ + @Handler + @SuppressWarnings("unused") + private void oplockBreakNotification(final OplockBreakNotification oplockBreakNotification) { + try { + + final SMB2FileId fileId = oplockBreakNotification.getFileId(); + final SMB2OplockLevel oplockLevel = oplockBreakNotification.getOplockLevel(); + // Check should this DiskShare handle this oplock break notification. If not, just ignore. + if (openedOplockFileId.contains(fileId)) { + logger.debug("FileId {} received OplockBreakNotification, Oplock level {}", fileId, + oplockLevel); + if (notificationHandler != null) { + // Preventing the improper use of handler (holding the thread). if holding thread, timeout exception will be throw. + // submit to taskQueue only when this DiskShare opened a handle with this fileId. Otherwise, ignore it. + taskQueue.execute(new Runnable() { + @Override + public void run() { + notificationHandler.handleOplockBreakNotification(oplockBreakNotification); + } + }); + } else { + logger.warn( + "FileId {}, NotificationHandler not exist to handle Oplock Break. On treeId = {}", + fileId, this.treeId); + throw new IllegalStateException( + "NotificationHandler not exist to handle Oplock Break."); + } + } + } catch (Throwable t) { + logger.error("Handling oplockBreakNotification error occur : ", t); + throw t; + } + } + + /*** + * Async create request handler. + * + * @param asyncCreateRequestNotification filePath with the corresponding messageId. + */ + @Handler + @SuppressWarnings("unused") + private void createRequestNotification(final AsyncCreateRequestNotification asyncCreateRequestNotification) { + try { + // Checking treeId can always map a DiskShare for AsyncCreateRequestNotification, because this happens before sending message. + if (asyncCreateRequestNotification.getSessionId() == this.sessionId + && asyncCreateRequestNotification.getTreeId() == this.treeId) { + if (notificationHandler != null) { + // Preventing the improper use of handler (holding the thread). if holding thread, timeout exception will be throw. + // submit to taskQueue only when sessionId and treeId match. Otherwise, ignore it. + taskQueue.execute(new Runnable() { + @Override + public void run() { + notificationHandler.handleAsyncCreateRequestNotification(asyncCreateRequestNotification); + } + }); + } else { + logger.debug("NotificationHandler not exist to handle asyncCreateRequestNotification. On treeId = {}", this.treeId); + } + } else { + logger.debug("asyncCreateRequestNotification ignored. this.treeId = {}, notification.getTreeId() = {}", this.treeId, asyncCreateRequestNotification.getTreeId()); + } + } catch (Throwable t) { + logger.error("Handling createRequestNotification error occur : ", t); + throw t; + } + } + + /*** + * Async create response handler. This is also a oplock related handler. + * Passing the createResponse Future to the client. + * This is also intended to prevent oplock break too fast and not able to handle oplock break notification properly. + * Notify the client oplock is granted on createResponse but still under processing. + * + * @param asyncCreateResponseNotification the corresponding messageId and fileId with the Future of createResponse. + */ + @Handler + @SuppressWarnings("unused") + private void createResponseNotification(final AsyncCreateResponseNotification asyncCreateResponseNotification) { + try { + // No matter the notificationHandler is set or not. Always try to remove the messageId from the Set. + boolean shouldHandle = asyncOperationMessageId.remove(asyncCreateResponseNotification.getMessageId()); + // Check should this DiskShare handle the create response. If not, just ignore. + if (shouldHandle) { + if (notificationHandler != null) { + // Preventing the improper use of handler (holding the thread). if holding thread, timeout exception will be throw. + // submit to taskQueue only if createRequest is sent out by this DiskShare. Otherwise, ignore it. + taskQueue.execute(new Runnable() { + @Override + public void run() { + notificationHandler.handleAsyncCreateResponseNotification(asyncCreateResponseNotification); + } + }); + } else { + logger.debug("NotificationHandler not exist to handle asyncCreateResponseNotification. On treeId = {}", this.treeId); + } + } else { + logger.debug("asyncCreateResponseNotification ignored. MessageId = {}, is not handled by this.treeId = {}", asyncCreateResponseNotification.getMessageId(), this.treeId); + } + } catch (Throwable t) { + logger.error("Handling createResponseNotification error occur : ", t); + throw t; + } + } + @Override public String toString() { return getClass().getSimpleName() + "[" + getSmbPath() + "]"; } /** - * A return object for the {@link #createFileAndResolve(SmbPath, SMB2ImpersonationLevel, Set, Set, Set, SMB2CreateDisposition, Set)} call. + * A return object for the {@link #createFileAndResolve(SmbPath, SMB2OplockLevel, SMB2ImpersonationLevel, Set, Set, Set, SMB2CreateDisposition, Set)} call. * * This object wraps the {@link SMB2CreateResponse} and the actual {@link Share} which generated it if the path needed to be resolved. */ - static class SMB2CreateResponseContext { + public static class SMB2CreateResponseContext { final SMB2CreateResponse resp; final DiskShare share; @@ -453,4 +733,27 @@ public SMB2CreateResponseContext(SMB2CreateResponse resp, DiskShare share) { this.share = share; } } + + /** + * A return object for the {@link #openWithResponse(String, SMB2OplockLevel, SMB2ImpersonationLevel, Set, Set, Set, SMB2CreateDisposition, Set)} call. + * + * This object wraps the {@link SMB2CreateResponse} and the diskEntry instance {@link DiskEntry}. + */ + public static class SMB2CreateResponseDiskEntry { + final SMB2CreateResponse resp; + final DiskEntry diskEntry; + + public SMB2CreateResponseDiskEntry(SMB2CreateResponse resp, DiskEntry diskEntry) { + this.resp = resp; + this.diskEntry = diskEntry; + } + + public SMB2CreateResponse getCreateResponse() { + return resp; + } + + public DiskEntry getDiskEntry() { + return diskEntry; + } + } } diff --git a/src/main/java/com/hierynomus/smbj/share/PipeShare.java b/src/main/java/com/hierynomus/smbj/share/PipeShare.java index 48d5bcea..72cf18cb 100644 --- a/src/main/java/com/hierynomus/smbj/share/PipeShare.java +++ b/src/main/java/com/hierynomus/smbj/share/PipeShare.java @@ -90,14 +90,22 @@ public boolean waitForPipe(String name, long timeout, TimeUnit timeoutUnit) { } public NamedPipe open(String name, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { + return open(name, null, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions); + } + + public NamedPipe open(String name, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set attributes, Set shareAccesses, SMB2CreateDisposition createDisposition, Set createOptions) { SmbPath path = new SmbPath(smbPath, name); - SMB2FileId response = super.openFileId(path, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions); + SMB2FileId response = super.openFileId(path, oplockLevel, impersonationLevel, accessMask, attributes, shareAccesses, createDisposition, createOptions); return new NamedPipe(response, this, path); } public SMB2FileId openFileId(String name, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { + return openFileId(name, null, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); + } + + public SMB2FileId openFileId(String name, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { SmbPath path = new SmbPath(smbPath, name); - return super.openFileId(path, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); + return super.openFileId(path, oplockLevel, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions); } public void closeFileId(SMB2FileId fileId) throws SMBApiException { diff --git a/src/main/java/com/hierynomus/smbj/share/PrinterShare.java b/src/main/java/com/hierynomus/smbj/share/PrinterShare.java index ad0a9592..6e968a37 100644 --- a/src/main/java/com/hierynomus/smbj/share/PrinterShare.java +++ b/src/main/java/com/hierynomus/smbj/share/PrinterShare.java @@ -49,6 +49,7 @@ public void print(ByteChunkProvider provider) { public void print(ByteChunkProvider provider, ProgressListener progressListener) { SMB2FileId fileId = openFileId(null, + null, null, EnumSet.of(AccessMask.FILE_WRITE_DATA), EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL), diff --git a/src/main/java/com/hierynomus/smbj/share/Share.java b/src/main/java/com/hierynomus/smbj/share/Share.java index 8227b4a4..1c171da5 100644 --- a/src/main/java/com/hierynomus/smbj/share/Share.java +++ b/src/main/java/com/hierynomus/smbj/share/Share.java @@ -30,6 +30,7 @@ import com.hierynomus.smbj.common.SmbPath; import com.hierynomus.smbj.connection.Connection; import com.hierynomus.smbj.connection.NegotiatedProtocol; +import com.hierynomus.smbj.event.handler.MessageIdCallback; import com.hierynomus.smbj.io.ArrayByteChunkProvider; import com.hierynomus.smbj.io.ByteChunkProvider; import com.hierynomus.smbj.io.EmptyByteChunkProvider; @@ -55,7 +56,7 @@ public class Share implements AutoCloseable { protected final SmbPath smbPath; protected final TreeConnect treeConnect; - private final long treeId; + protected final long treeId; protected Session session; private final SMB2Dialect dialect; private final int readBufferSize; @@ -64,7 +65,7 @@ public class Share implements AutoCloseable { private final long writeTimeout; private final int transactBufferSize; private final long transactTimeout; - private final long sessionId; + protected final long sessionId; private final AtomicBoolean disconnected = new AtomicBoolean(false); Share(SmbPath smbPath, TreeConnect treeConnect) { @@ -116,14 +117,15 @@ int getWriteBufferSize() { return writeBufferSize; } - SMB2FileId openFileId(SmbPath path, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { - return createFile(path, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions).getFileId(); + SMB2FileId openFileId(SmbPath path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { + return createFile(path, oplockLevel, impersonationLevel, accessMask, fileAttributes, shareAccess, createDisposition, createOptions).getFileId(); } - SMB2CreateResponse createFile(SmbPath path, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { + SMB2CreateResponse createFile(SmbPath path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions) { SMB2CreateRequest cr = new SMB2CreateRequest( dialect, sessionId, treeId, + oplockLevel, impersonationLevel, accessMask, fileAttributes, @@ -136,6 +138,22 @@ SMB2CreateResponse createFile(SmbPath path, SMB2ImpersonationLevel impersonation return resp; } + Future createAsync(SmbPath path, SMB2OplockLevel oplockLevel, SMB2ImpersonationLevel impersonationLevel, Set accessMask, Set fileAttributes, Set shareAccess, SMB2CreateDisposition createDisposition, Set createOptions, MessageIdCallback messageIdCallback) { + SMB2CreateRequest cr = new SMB2CreateRequest( + dialect, + sessionId, treeId, + oplockLevel, + impersonationLevel, + accessMask, + fileAttributes, + shareAccess, + createDisposition, + createOptions, + path + ); + return send(cr, messageIdCallback); + } + protected Set getCreateSuccessStatus() { return SUCCESS_OR_SYMLINK; } @@ -317,18 +335,31 @@ private Future ioctlAsync(SMB2FileId fileId, long ctlCode, bo return send(ioreq); } + SMB2OplockBreakAcknowledgmentResponse sendOplockBreakAcknowledgment(SMB2FileId fileId, SMB2OplockLevel oplockLevel) { + SMB2OplockBreakAcknowledgment qreq = new SMB2OplockBreakAcknowledgment( + dialect, + sessionId, treeId, + oplockLevel, fileId + ); + return sendReceive(qreq, "OplockBreakAck", fileId, SUCCESS, transactTimeout); + } + private T sendReceive(SMB2Packet request, String name, Object target, Set successResponses, long timeout) { Future fut = send(request); return receive(fut, name, target, successResponses, timeout); } private Future send(SMB2Packet request) { + return send(request, null); + } + + private Future send(SMB2Packet request, MessageIdCallback messageIdCallback) { if (!isConnected()) { throw new SMBRuntimeException(getClass().getSimpleName() + " has already been closed"); } try { - return session.send(request); + return session.send(request, messageIdCallback); } catch (TransportException e) { throw new SMBRuntimeException(e); } diff --git a/src/main/java/com/hierynomus/smbj/share/TreeConnect.java b/src/main/java/com/hierynomus/smbj/share/TreeConnect.java index 04672a63..678332ed 100644 --- a/src/main/java/com/hierynomus/smbj/share/TreeConnect.java +++ b/src/main/java/com/hierynomus/smbj/share/TreeConnect.java @@ -42,16 +42,16 @@ public class TreeConnect { private Session session; private final Set capabilities; private Connection connection; - private final SMBEventBus bus; + private final SMBEventBus connectionPrivateBus; private final Set maximalAccess; - public TreeConnect(long treeId, SmbPath smbPath, Session session, Set capabilities, Connection connection, SMBEventBus bus, Set maximalAccess) { + public TreeConnect(long treeId, SmbPath smbPath, Session session, Set capabilities, Connection connection, SMBEventBus connectionPrivateBus, Set maximalAccess) { this.treeId = treeId; this.smbPath = smbPath; this.session = session; this.capabilities = capabilities; this.connection = connection; - this.bus = bus; + this.connectionPrivateBus = connectionPrivateBus; this.maximalAccess = maximalAccess; } @@ -68,7 +68,7 @@ void close() throws TransportException { throw new SMBApiException(smb2Packet.getHeader(), "Error closing connection to " + smbPath); } } finally { - bus.publish(new TreeDisconnected(session.getSessionId(), treeId)); + connectionPrivateBus.publish(new TreeDisconnected(session.getSessionId(), treeId)); } }