• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 28898324220

07 Jul 2026 09:00PM UTC coverage: 95.729% (+0.9%) from 94.813%
28898324220

Pull #105

github

web-flow
Merge 287f920bd into 4b2f45149
Pull Request #105: WIP feat: Implement IETF Resumable Uploads for HTTP (RUFH) Protocol

1103 of 1206 branches covered (91.46%)

Branch coverage included in aggregate %.

766 of 777 new or added lines in 42 files covered. (98.58%)

2573 of 2634 relevant lines covered (97.68%)

6.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.59
/src/main/java/me/desair/tus/server/digest/HttpDigestsPostPutPatchRequestHandler.java
1
package me.desair.tus.server.digest;
2

3
import java.io.IOException;
4
import java.io.InputStream;
5
import java.security.DigestInputStream;
6
import java.security.MessageDigest;
7
import java.util.Map;
8
import java.util.Objects;
9
import me.desair.tus.server.HttpHeader;
10
import me.desair.tus.server.HttpMethod;
11
import me.desair.tus.server.HttpProblemDetails;
12
import me.desair.tus.server.checksum.ChecksumAlgorithm;
13
import me.desair.tus.server.digest.validation.HttpDigestsValidator;
14
import me.desair.tus.server.exception.TusException;
15
import me.desair.tus.server.exception.UploadDigestMismatchException;
16
import me.desair.tus.server.upload.UploadInfo;
17
import me.desair.tus.server.upload.UploadLockingService;
18
import me.desair.tus.server.upload.UploadStorageService;
19
import me.desair.tus.server.util.AbstractRequestHandler;
20
import me.desair.tus.server.util.TusServletRequest;
21
import me.desair.tus.server.util.TusServletResponse;
22
import me.desair.tus.server.util.Utils;
23
import org.apache.commons.codec.binary.Base64;
24
import org.apache.commons.lang3.StringUtils;
25
import org.apache.commons.lang3.Strings;
26

27
/** Unified request handler for RFC 9530 HTTP Digests verification in RUFH protocol. */
28
public class HttpDigestsPostPutPatchRequestHandler extends AbstractRequestHandler {
6✔
29

30
  @Override
31
  public boolean supports(HttpMethod method) {
32
    return HttpMethod.POST.equals(method)
14✔
33
        || HttpMethod.PUT.equals(method)
8✔
34
        || HttpMethod.PATCH.equals(method);
8✔
35
  }
36

37
  @Override
38
  public HttpProblemDetails process(
39
      HttpMethod method,
40
      TusServletRequest servletRequest,
41
      TusServletResponse servletResponse,
42
      UploadStorageService uploadStorageService,
43
      UploadLockingService uploadLockingService,
44
      String ownerKey,
45
      TusException exception)
46
      throws IOException, TusException {
47

48
    // 1. Verify Content-Digest for the request chunk
49
    verifyContentDigest(method, servletRequest);
4✔
50

51
    // Resolve UploadInfo using centralized helpers
52
    String uploadUri = Utils.getUploadUri(method, servletRequest, servletResponse);
5✔
53
    UploadInfo uploadInfo = null;
2✔
54
    if (uploadUri != null) {
2✔
55
      try {
56
        uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey);
5✔
NEW
57
      } catch (Exception e) {
×
58
        // Ignored
59
      }
1✔
60
    }
61

62
    if (uploadInfo != null) {
2✔
63
      // 2. Capture client Repr-Digest and Want-Repr-Digest
64
      captureDigestPreferences(servletRequest, uploadInfo, uploadStorageService);
5✔
65

66
      // 3. Verify Repr-Digest on upload completion
67
      verifyReprDigestOnCompletion(uploadInfo, uploadUri, ownerKey, uploadStorageService);
6✔
68

69
      // 4. Provide Repr-Digest response header if requested
70
      addReprDigestResponseHeader(
7✔
71
          servletResponse, uploadInfo, uploadUri, ownerKey, uploadStorageService);
72
    }
73

74
    return null;
2✔
75
  }
76

77
  private void verifyContentDigest(HttpMethod method, TusServletRequest servletRequest)
78
      throws TusException, IOException {
79
    String headerVal = servletRequest.getHeader(HttpHeader.CONTENT_DIGEST);
4✔
80
    if (StringUtils.isBlank(headerVal)) {
3✔
81
      return;
1✔
82
    }
83

84
    // Revalidate trailing header syntax
85
    new HttpDigestsValidator().validate(method, servletRequest, null, null);
8✔
86

87
    Map<ChecksumAlgorithm, String> expectedDigests = ChecksumAlgorithm.parseDigestHeader(headerVal);
3✔
88
    for (Map.Entry<ChecksumAlgorithm, String> entry : expectedDigests.entrySet()) {
11✔
89
      ChecksumAlgorithm alg = entry.getKey();
4✔
90
      String expectedValue = entry.getValue();
4✔
91
      String calculatedValue = servletRequest.getCalculatedChecksum(alg);
4✔
92

93
      if (calculatedValue != null && !Strings.CS.equals(expectedValue, calculatedValue)) {
7!
94
        throw new UploadDigestMismatchException(
3✔
95
            "Content-Digest mismatch for algorithm "
96
                + alg.getHttpDigestNames().get(0)
9✔
97
                + ". Expected: "
98
                + expectedValue
99
                + " but was: "
100
                + calculatedValue);
101
      }
102
    }
1✔
103
  }
1✔
104

105
  private void captureDigestPreferences(
106
      TusServletRequest servletRequest,
107
      UploadInfo uploadInfo,
108
      UploadStorageService uploadStorageService)
109
      throws IOException, TusException {
110
    boolean updated = false;
2✔
111

112
    // Capture Repr-Digest
113
    String reprDigestHeader = servletRequest.getHeader(HttpHeader.REPR_DIGEST);
4✔
114
    if (StringUtils.isNotBlank(reprDigestHeader) && uploadInfo.getRepresentationDigest() == null) {
6!
115
      uploadInfo.setRepresentationDigest(reprDigestHeader);
3✔
116
      updated = true;
2✔
117
    }
118

119
    // Capture Want-Repr-Digest
120
    String wantReprDigestHeader = servletRequest.getHeader(HttpHeader.WANT_REPR_DIGEST);
4✔
121
    if (StringUtils.isNotBlank(wantReprDigestHeader)
4✔
122
        && uploadInfo.getRequestedRepresentationDigests() == null) {
2!
123
      uploadInfo.setRequestedRepresentationDigests(wantReprDigestHeader);
3✔
124
      updated = true;
2✔
125
    }
126

127
    if (updated) {
2✔
128
      uploadStorageService.update(uploadInfo);
3✔
129
    }
130
  }
1✔
131

132
  private void verifyReprDigestOnCompletion(
133
      UploadInfo uploadInfo,
134
      String uploadUri,
135
      String ownerKey,
136
      UploadStorageService uploadStorageService)
137
      throws IOException, TusException {
138

139
    if (!uploadInfo.isUploadInProgress() && uploadInfo.getRepresentationDigest() != null) {
6✔
140
      Map<ChecksumAlgorithm, String> expectedDigests =
1✔
141
          ChecksumAlgorithm.parseDigestHeader(uploadInfo.getRepresentationDigest());
3✔
142

143
      for (Map.Entry<ChecksumAlgorithm, String> entry : expectedDigests.entrySet()) {
11✔
144
        ChecksumAlgorithm alg = entry.getKey();
4✔
145
        String expectedValue = entry.getValue();
4✔
146
        String calculatedValue =
5✔
147
            calculateEntireFileDigest(uploadUri, ownerKey, alg, uploadStorageService);
2✔
148

149
        if (calculatedValue != null) {
2!
150
          if (!Strings.CS.equals(expectedValue, calculatedValue)) {
5✔
151
            throw new UploadDigestMismatchException(
3✔
152
                "Repr-Digest mismatch for algorithm "
153
                    + alg.getHttpDigestNames().get(0)
9✔
154
                    + ". Expected: "
155
                    + expectedValue
156
                    + " but was: "
157
                    + calculatedValue);
158
          } else {
159
            // Deduplication support: If verification succeeds and deduplication is enabled
160
            if (uploadStorageService.isUploadDeduplicationEnabled()
4!
161
                && uploadInfo.getDuplicatesUploadId() == null) {
2!
162
              UploadInfo duplicateInfo =
3✔
163
                  uploadStorageService.getUploadInfoByChecksum(expectedValue, alg);
2✔
164
              if (duplicateInfo != null
3✔
165
                  && !Objects.equals(duplicateInfo.getId(), uploadInfo.getId())) {
5!
166
                uploadInfo.setDuplicatesUploadId(duplicateInfo.getId());
4✔
167
                uploadStorageService.update(uploadInfo);
4✔
168
              } else {
169
                // Index this file
170
                uploadInfo.setChecksum(expectedValue);
3✔
171
                uploadInfo.setChecksumAlgorithm(alg);
3✔
172
                uploadStorageService.update(uploadInfo);
3✔
173
              }
174
            }
175
          }
176
        }
177
      }
1✔
178
    }
179
  }
1✔
180

181
  private void addReprDigestResponseHeader(
182
      TusServletResponse servletResponse,
183
      UploadInfo uploadInfo,
184
      String uploadUri,
185
      String ownerKey,
186
      UploadStorageService uploadStorageService)
187
      throws IOException, TusException {
188

189
    String requestedDigests = uploadInfo.getRequestedRepresentationDigests();
3✔
190
    if (StringUtils.isNotBlank(requestedDigests)) {
3✔
191
      ChecksumAlgorithm preferredAlg = ChecksumAlgorithm.selectBestAlgorithm(requestedDigests);
3✔
192
      if (preferredAlg != null) {
2!
193
        String calculatedValue =
5✔
194
            calculateEntireFileDigest(uploadUri, ownerKey, preferredAlg, uploadStorageService);
2✔
195
        if (calculatedValue != null) {
2!
196
          servletResponse.setHeader(
4✔
197
              HttpHeader.REPR_DIGEST,
198
              preferredAlg.getHttpDigestNames().get(0) + "=:" + calculatedValue + ":");
6✔
199
        }
200
      }
201
    }
202
  }
1✔
203

204
  private String calculateEntireFileDigest(
205
      String uploadUri,
206
      String ownerKey,
207
      ChecksumAlgorithm alg,
208
      UploadStorageService uploadStorageService)
209
      throws IOException, TusException {
210
    MessageDigest md = alg.getMessageDigest();
3✔
211
    if (md == null) {
2!
NEW
212
      return null;
×
213
    }
214
    try (InputStream is = uploadStorageService.getUploadedBytes(uploadUri, ownerKey)) {
5✔
215
      if (is == null) {
2!
NEW
216
        return null;
×
217
      }
218
      try (DigestInputStream dis = new DigestInputStream(is, md)) {
6✔
219
        byte[] buffer = new byte[8192];
3✔
220
        while (dis.read(buffer) != -1) {
6✔
221
          // do nothing, let stream update digest
222
        }
223
      }
NEW
224
    }
×
225
    return Base64.encodeBase64String(md.digest());
4✔
226
  }
227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc