001/* ***** BEGIN LICENSE BLOCK ***** 002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 003 * 004 * The contents of this file are subject to the Mozilla Public License Version 005 * 1.1 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * http://www.mozilla.org/MPL/ 008 * 009 * Software distributed under the License is distributed on an "AS IS" basis, 010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 011 * for the specific language governing rights and limitations under the 012 * License. 013 * 014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in 015 * Java(TM), hosted at https://github.com/gunterze/dcm4che. 016 * 017 * The Initial Developer of the Original Code is 018 * Agfa Healthcare. 019 * Portions created by the Initial Developer are Copyright (C) 2011 020 * the Initial Developer. All Rights Reserved. 021 * 022 * Contributor(s): 023 * See @authors listed below 024 * 025 * Alternatively, the contents of this file may be used under the terms of 026 * either the GNU General Public License Version 2 or later (the "GPL"), or 027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 028 * in which case the provisions of the GPL or the LGPL are applicable instead 029 * of those above. If you wish to allow use of your version of this file only 030 * under the terms of either the GPL or the LGPL, and not to allow others to 031 * use your version of this file under the terms of the MPL, indicate your 032 * decision by deleting the provisions above and replace them with the notice 033 * and other provisions required by the GPL or the LGPL. If you do not delete 034 * the provisions above, a recipient may use your version of this file under 035 * the terms of any one of the MPL, the GPL or the LGPL. 036 * 037 * ***** END LICENSE BLOCK ***** */ 038 039package org.dcm4che3.net; 040 041import java.io.EOFException; 042import java.io.IOException; 043import java.io.InputStream; 044import java.io.OutputStream; 045import java.util.ArrayList; 046import java.util.Arrays; 047 048import org.dcm4che3.data.Tag; 049import org.dcm4che3.data.UID; 050import org.dcm4che3.data.Attributes; 051import org.dcm4che3.io.DicomInputStream; 052import org.dcm4che3.net.pdu.AAbort; 053import org.dcm4che3.net.pdu.AAssociateAC; 054import org.dcm4che3.net.pdu.AAssociateRJ; 055import org.dcm4che3.net.pdu.AAssociateRQ; 056import org.dcm4che3.net.pdu.AAssociateRQAC; 057import org.dcm4che3.net.pdu.CommonExtendedNegotiation; 058import org.dcm4che3.net.pdu.ExtendedNegotiation; 059import org.dcm4che3.net.pdu.PresentationContext; 060import org.dcm4che3.net.pdu.RoleSelection; 061import org.dcm4che3.net.pdu.UserIdentityAC; 062import org.dcm4che3.net.pdu.UserIdentityRQ; 063import org.dcm4che3.util.ByteUtils; 064import org.dcm4che3.util.SafeClose; 065import org.dcm4che3.util.StreamUtils; 066 067/** 068 * @author Gunter Zeilinger <gunterze@gmail.com> 069 * 070 */ 071class PDUDecoder extends PDVInputStream { 072 073 private static final String UNRECOGNIZED_PDU = 074 "{}: unrecognized PDU[type={}, len={}]"; 075 private static final String INVALID_PDU_LENGTH = 076 "{}: invalid length of PDU[type={}, len={}]"; 077 private static final String INVALID_COMMON_EXTENDED_NEGOTIATION = 078 "{}: invalid Common Extended Negotiation sub-item in PDU[type={}, len={}]"; 079 private static final String INVALID_USER_IDENTITY = 080 "{}: invalid User Identity sub-item in PDU[type={}, len={}]"; 081 private static final String INVALID_PDV = 082 "{}: invalid PDV in PDU[type={}, len={}]"; 083 private static final String UNEXPECTED_PDV_TYPE = 084 "{}: unexpected PDV type in PDU[type={}, len={}]"; 085 private static final String UNEXPECTED_PDV_PCID = 086 "{}: unexpected pcid in PDV in PDU[type={}, len={}]"; 087 088 private static final int MAX_PDU_LEN = 0x1000000; // 16MiB 089 090 private final Association as; 091 private final InputStream in; 092 private final Thread th; 093 private byte[] buf = new byte[6 + Connection.DEF_MAX_PDU_LENGTH]; 094 private int pos; 095 private int pdutype; 096 private int pdulen; 097 private int pcid = -1; 098 private int pdvmch; 099 private int pdvend; 100 101 public PDUDecoder(Association as, InputStream in) { 102 this.as = as; 103 this.in = in; 104 this.th = Thread.currentThread(); 105 } 106 107 private int remaining() { 108 return pdulen + 6 - pos; 109 } 110 111 private boolean hasRemaining() { 112 return pos < pdulen + 6; 113 } 114 115 private int get() { 116 if (!hasRemaining()) 117 throw new IndexOutOfBoundsException(); 118 return buf[pos++] & 0xFF; 119 } 120 121 private void get(byte[] b, int off, int len) { 122 if (len > remaining()) 123 throw new IndexOutOfBoundsException(); 124 System.arraycopy(buf, pos, b, off, len); 125 pos += len; 126 } 127 128 private void skip(int len) { 129 if (len > remaining()) 130 throw new IndexOutOfBoundsException(); 131 pos += len; 132 } 133 134 private int getUnsignedShort() { 135 int val = ByteUtils.bytesToUShortBE(buf, pos); 136 pos += 2; 137 return val; 138 } 139 140 private int getInt() { 141 int val = ByteUtils.bytesToIntBE(buf, pos); 142 pos += 4; 143 return val; 144 } 145 146 private byte[] getBytes(int len) { 147 byte[] bs = new byte[len]; 148 get(bs, 0, len); 149 return bs; 150 } 151 152 private byte[] decodeBytes() { 153 return getBytes(getUnsignedShort()); 154 } 155 156 public void nextPDU() throws IOException { 157 checkThread(); 158 Association.LOG.trace("{}: waiting for PDU", as); 159 readFully(0, 10); 160 pos = 0; 161 pdutype = get(); 162 get(); 163 pdulen = getInt(); 164 Association.LOG.trace("{} >> PDU[type={}, len={}]", 165 new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL }); 166 switch (pdutype) { 167 case PDUType.A_ASSOCIATE_RQ: 168 readPDU(); 169 as.onAAssociateRQ((AAssociateRQ) decode(new AAssociateRQ())); 170 return; 171 case PDUType.A_ASSOCIATE_AC: 172 readPDU(); 173 as.onAAssociateAC((AAssociateAC) decode(new AAssociateAC())); 174 return; 175 case PDUType.P_DATA_TF: 176 readPDU(); 177 as.onPDataTF(); 178 return; 179 case PDUType.A_ASSOCIATE_RJ: 180 checkPDULength(4); 181 get(); 182 as.onAAssociateRJ(new AAssociateRJ(get(), get(), get())); 183 break; 184 case PDUType.A_RELEASE_RQ: 185 checkPDULength(4); 186 as.onAReleaseRQ(); 187 break; 188 case PDUType.A_RELEASE_RP: 189 checkPDULength(4); 190 as.onAReleaseRP(); 191 break; 192 case PDUType.A_ABORT: 193 checkPDULength(4); 194 get(); 195 get(); 196 as.onAAbort(new AAbort(get(), get())); 197 break; 198 default: 199 abort(AAbort.UNRECOGNIZED_PDU, UNRECOGNIZED_PDU); 200 } 201 } 202 203 private void checkThread() { 204 if (th != Thread.currentThread()) 205 throw new IllegalStateException("Entered by wrong thread"); 206 } 207 208 private void checkPDULength(int len) throws AAbort { 209 if (pdulen != len) 210 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH); 211 } 212 213 private void readPDU() throws IOException { 214 if (pdulen < 4 || pdulen > MAX_PDU_LEN) 215 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH); 216 217 if (6 + pdulen > buf.length) 218 buf = Arrays.copyOf(buf, 6 + pdulen); 219 220 readFully(10, pdulen - 4); 221 } 222 223 private void readFully(int off, int len) throws IOException { 224 try { 225 StreamUtils.readFully(in, buf, off, len); 226 } catch (IOException e) { 227 throw e; 228 } 229 } 230 231 private void abort(int reason, String logmsg) throws AAbort { 232 Association.LOG.warn(logmsg, 233 new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL }); 234 throw new AAbort(AAbort.UL_SERIVE_PROVIDER, reason); 235 } 236 237 @SuppressWarnings("deprecation") 238 private String getString(int len) { 239 if (pos + len > pdulen + 6) 240 throw new IndexOutOfBoundsException(); 241 String s; 242 // Skip illegal trailing NULL 243 int len0 = len; 244 while (len0 > 0 && buf[pos + len0 - 1] == 0) { 245 len0--; 246 } 247 s = new String(buf, 0, pos, len0); 248 pos += len; 249 return s; 250 } 251 252 private String decodeString() { 253 return getString(getUnsignedShort()); 254 } 255 256 private AAssociateRQAC decode(AAssociateRQAC rqac) 257 throws AAbort { 258 try { 259 rqac.setProtocolVersion(getUnsignedShort()); 260 get(); 261 get(); 262 rqac.setCalledAET(getString(16).trim()); 263 rqac.setCallingAET(getString(16).trim()); 264 rqac.setReservedBytes(getBytes(32)); 265 while (pos < pdulen) 266 decodeItem(rqac); 267 checkPDULength(pos - 6); 268 } catch (IndexOutOfBoundsException e) { 269 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH); 270 } 271 return rqac; 272 } 273 274 private void decodeItem(AAssociateRQAC rqac) throws AAbort { 275 int itemType = get(); 276 get(); // skip reserved byte 277 int itemLen = getUnsignedShort(); 278 switch (itemType) 279 { 280 case ItemType.APP_CONTEXT: 281 rqac.setApplicationContext(getString(itemLen)); 282 break; 283 case ItemType.RQ_PRES_CONTEXT: 284 case ItemType.AC_PRES_CONTEXT: 285 rqac.addPresentationContext(decodePC(itemLen)); 286 break; 287 case ItemType.USER_INFO: 288 decodeUserInfo(itemLen, rqac); 289 break; 290 default: 291 skip(itemLen); 292 } 293 } 294 295 private PresentationContext decodePC(int itemLen) { 296 int pcid = get(); 297 get(); // skip reserved byte 298 int result = get(); 299 get(); // skip reserved byte 300 String as = null; 301 ArrayList<String> tss = new ArrayList<String>(1); 302 int endpos = pos + itemLen - 4; 303 while (pos < endpos) { 304 int subItemType = get() & 0xff; 305 get(); // skip reserved byte 306 int subItemLen = getUnsignedShort(); 307 switch (subItemType) 308 { 309 case ItemType.ABSTRACT_SYNTAX: 310 as = getString(subItemLen); 311 break; 312 case ItemType.TRANSFER_SYNTAX: 313 tss.add(getString(subItemLen)); 314 break; 315 default: 316 skip(subItemLen); 317 } 318 } 319 return new PresentationContext(pcid, result, as, 320 tss.toArray(new String[tss.size()])); 321 } 322 323 private void decodeUserInfo(int itemLength, AAssociateRQAC rqac) throws AAbort 324 { 325 int endpos = pos + itemLength; 326 while (pos < endpos) 327 decodeUserInfoSubItem(rqac); 328 } 329 330 private void decodeUserInfoSubItem(AAssociateRQAC rqac) throws AAbort 331 { 332 int itemType = get(); 333 get(); // skip reserved byte 334 int itemLen = getUnsignedShort(); 335 switch (itemType) 336 { 337 case ItemType.MAX_PDU_LENGTH: 338 rqac.setMaxPDULength(getInt()); 339 break; 340 case ItemType.IMPL_CLASS_UID: 341 rqac.setImplClassUID(getString(itemLen)); 342 break; 343 case ItemType.ASYNC_OPS_WINDOW: 344 rqac.setMaxOpsInvoked(getUnsignedShort()); 345 rqac.setMaxOpsPerformed(getUnsignedShort()); 346 break; 347 case ItemType.ROLE_SELECTION: 348 rqac.addRoleSelection(decodeRoleSelection(itemLen)); 349 break; 350 case ItemType.IMPL_VERSION_NAME: 351 rqac.setImplVersionName(getString(itemLen)); 352 break; 353 case ItemType.EXT_NEG: 354 rqac.addExtendedNegotiation(decodeExtNeg(itemLen)); 355 break; 356 case ItemType.COMMON_EXT_NEG: 357 rqac.addCommonExtendedNegotiation(decodeCommonExtNeg(itemLen)); 358 break; 359 case ItemType.RQ_USER_IDENTITY: 360 rqac.setUserIdentityRQ(decodeUserIdentityRQ(itemLen)); 361 break; 362 case ItemType.AC_USER_IDENTITY: 363 rqac.setUserIdentityAC(decodeUserIdentityAC(itemLen)); 364 break; 365 default: 366 skip(itemLen); 367 } 368 } 369 370 private RoleSelection decodeRoleSelection(int itemLen) { 371 String cuid = decodeString(); 372 boolean scu = get() != 0; 373 boolean scp = get() != 0; 374 return new RoleSelection(cuid, scu, scp); 375 } 376 377 private ExtendedNegotiation decodeExtNeg(int itemLen) { 378 int uidLength = getUnsignedShort(); 379 String cuid = getString(uidLength); 380 byte[] info = getBytes(itemLen - uidLength - 2); 381 return new ExtendedNegotiation(cuid, info); 382 } 383 384 private CommonExtendedNegotiation decodeCommonExtNeg(int itemLen) 385 throws AAbort { 386 int endPos = pos + itemLen; 387 String sopCUID = getString(getUnsignedShort()); 388 String serviceCUID = getString(getUnsignedShort()); 389 ArrayList<String> relSopCUIDs = new ArrayList<String>(1); 390 int relSopCUIDsLen = getUnsignedShort(); 391 int endRelSopCUIDs = pos + relSopCUIDsLen; 392 while (pos < endRelSopCUIDs) 393 relSopCUIDs.add(decodeString()); 394 if (pos != endRelSopCUIDs || pos > endPos) 395 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, 396 INVALID_COMMON_EXTENDED_NEGOTIATION); 397 skip(endPos - pos); 398 return new CommonExtendedNegotiation(sopCUID, serviceCUID, 399 relSopCUIDs.toArray(new String[relSopCUIDs.size()])); 400 } 401 402 private UserIdentityRQ decodeUserIdentityRQ(int itemLen) throws AAbort { 403 int endPos = pos + itemLen; 404 int type = get() & 0xff; 405 boolean rspReq = get() != 0; 406 byte[] primaryField = decodeBytes(); 407 byte[] secondaryField = decodeBytes(); 408 if (pos != endPos) 409 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY); 410 return new UserIdentityRQ(type, rspReq, primaryField, secondaryField); 411 } 412 413 private UserIdentityAC decodeUserIdentityAC(int itemLen) throws AAbort { 414 int endPos = pos + itemLen; 415 byte[] serverResponse = decodeBytes(); 416 if (pos != endPos) 417 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY); 418 return new UserIdentityAC(serverResponse); 419 } 420 421 public void decodeDIMSE() throws IOException { 422 checkThread(); 423 if (pcid != - 1) 424 return; // already inside decodeDIMSE 425 426 nextPDV(PDVType.COMMAND, -1); 427 428 PresentationContext pc = as.getPresentationContext(pcid); 429 if (pc == null) { 430 Association.LOG.warn( 431 "{}: No Presentation Context with given ID - {}", 432 as, pcid); 433 throw new AAbort(); 434 } 435 436 if (!pc.isAccepted()) { 437 Association.LOG.warn( 438 "{}: No accepted Presentation Context with given ID - {}", 439 as, pcid); 440 throw new AAbort(); 441 } 442 443 Attributes cmd = readCommand(); 444 Dimse dimse = dimseOf(cmd); 445 String tsuid = pc.getTransferSyntax(); 446 if (Dimse.LOG.isInfoEnabled()) { 447 Dimse.LOG.info("{} >> {}", as, dimse.toString(cmd, pcid, tsuid)); 448 Dimse.LOG.debug("Command:\n{}", cmd); 449 } 450 if (dimse == Dimse.C_CANCEL_RQ) { 451 as.onCancelRQ(cmd); 452 } else if (Commands.hasDataset(cmd)) { 453 nextPDV(PDVType.DATA, pcid); 454 if (dimse.isRSP()) { 455 Attributes data = readDataset(tsuid); 456 Dimse.LOG.debug("Dataset:\n{}", data); 457 as.onDimseRSP(dimse, cmd, data); 458 } else { 459 as.onDimseRQ(pc, dimse, cmd, this); 460 long skipped = skipAll(); 461 if (skipped > 0) 462 Association.LOG.debug( 463 "{}: Service User did not consume {} bytes of DIMSE data.", 464 as, skipped); 465 } 466 skipAll(); 467 } else { 468 if (dimse.isRSP()) { 469 as.onDimseRSP(dimse, cmd, null); 470 } else { 471 as.onDimseRQ(pc, dimse, cmd, null); 472 } 473 } 474 pcid = -1; 475 } 476 477 private Dimse dimseOf(Attributes cmd) throws AAbort { 478 try { 479 return Dimse.valueOf(cmd.getInt(Tag.CommandField, 0)); 480 } catch (IllegalArgumentException e) { 481 Dimse.LOG.info("{}: illegal DIMSE:", as); 482 Dimse.LOG.info("\n{}", cmd); 483 throw new AAbort(); 484 } 485 } 486 487 private Attributes readCommand() throws IOException { 488 DicomInputStream in = 489 new DicomInputStream(this, UID.ImplicitVRLittleEndian); 490 try { 491 return in.readCommand(); 492 } finally { 493 SafeClose.close(in); 494 } 495 } 496 497 @Override 498 public Attributes readDataset(String tsuid) throws IOException { 499 DicomInputStream in = new DicomInputStream(this, tsuid); 500 try { 501 return in.readDataset(-1, -1); 502 } finally { 503 SafeClose.close(in); 504 } 505 } 506 507 private void nextPDV(int expectedPDVType, int expectedPCID) 508 throws IOException { 509 if (!hasRemaining()) { 510 nextPDU(); 511 if (pdutype != PDUType.P_DATA_TF) { 512 Association.LOG.info( 513 "{}: Expected P-DATA-TF PDU but received PDU[type={}]", 514 as, pdutype); 515 throw new EOFException(); 516 } 517 } 518 if (remaining() < 6) 519 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV); 520 int pdvlen = getInt(); 521 this.pdvend = pos + pdvlen; 522 if (pdvlen < 2 || pdvlen > remaining()) 523 abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV); 524 this.pcid = get(); 525 this.pdvmch = get(); 526 Association.LOG.trace("{} >> PDV[len={}, pcid={}, mch={}]", 527 new Object[] { as, pdvlen, pcid, pdvmch } ); 528 if ((pdvmch & PDVType.COMMAND) != expectedPDVType) 529 abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_TYPE); 530 if (expectedPCID != -1 && pcid != expectedPCID) 531 abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_PCID); 532 } 533 534 private boolean isLastPDV() throws IOException { 535 while (pos == pdvend) { 536 if ((pdvmch & PDVType.LAST) != 0) 537 return true; 538 nextPDV(pdvmch & PDVType.COMMAND, pcid); 539 } 540 return false; 541 } 542 543 @Override 544 public int read() throws IOException { 545 if (th != Thread.currentThread()) 546 throw new IllegalStateException("Entered by wrong thread"); 547 if (isLastPDV()) 548 return -1; 549 550 return get(); 551 } 552 553 @Override 554 public int read(byte[] b, int off, int len) throws IOException { 555 if (th != Thread.currentThread()) 556 throw new IllegalStateException("Entered by wrong thread"); 557 if (isLastPDV()) 558 return -1; 559 560 int read = Math.min(len, pdvend - pos); 561 get(b, off, read); 562 return read; 563 } 564 565 @Override 566 public final int available() { 567 return pdvend - pos; 568 } 569 570 @Override 571 public long skip(long n) throws IOException 572 { 573 if (th != Thread.currentThread()) 574 throw new IllegalStateException("Entered by wrong thread"); 575 if (n <= 0 || isLastPDV()) 576 return 0; 577 578 int skipped = (int) Math.min(n, pdvend - pos); 579 skip(skipped); 580 return skipped; 581 } 582 583 @Override 584 public void close() throws IOException { 585 if (th != Thread.currentThread()) 586 throw new IllegalStateException("Entered by wrong thread"); 587 skipAll(); 588 } 589 590 @Override 591 public long skipAll() throws IOException { 592 if (th != Thread.currentThread()) 593 throw new IllegalStateException("Entered by wrong thread"); 594 long n = 0; 595 while (!isLastPDV()) { 596 n += pdvend - pos; 597 pos = pdvend; 598 } 599 return n; 600 } 601 602 @Override 603 public void copyTo(OutputStream out, int length) throws IOException { 604 if (th != Thread.currentThread()) 605 throw new IllegalStateException("Entered by wrong thread"); 606 int remaining = length; 607 while (remaining > 0) { 608 if (isLastPDV()) 609 throw new EOFException("remaining: " + remaining); 610 int read = Math.min(remaining, pdvend - pos); 611 out.write(buf, pos, read); 612 remaining -= read; 613 pos += read; 614 } 615 } 616 617 @Override 618 public void copyTo(OutputStream out) throws IOException { 619 if (th != Thread.currentThread()) 620 throw new IllegalStateException("Entered by wrong thread"); 621 while (!isLastPDV()) { 622 out.write(buf, pos, pdvend - pos); 623 pos = pdvend; 624 } 625 } 626}