/// <summary> /// Gets sequenced JSON from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>JSON content</returns> public Dictionary <string, object> GetSeq(ResourceRef res, CancellationToken ct = default) { // Retrieve response from cache (if available). var key = res.Uri.AbsoluteUri; Response response_cache = null; lock (_lock) if (!TryGetValue(key, out response_cache)) { response_cache = null; } // Get instance source. var response_web = Xml.Response.Get( res: res, ct: ct, previous: response_cache); // Parse instance source JSON. var obj_web = (Dictionary <string, object>)eduJSON.Parser.Parse(response_web.Value, ct); if (response_web.IsFresh) { // Save response to cache. lock (_lock) this[key] = response_web; } return(obj_web); }
/// <summary> /// Gets sequenced JSON from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <returns>JSON content</returns> public Response GetSeqFromCache(ResourceRef res) { var key = res.Uri.AbsoluteUri; lock (Lock) return(TryGetValue(key, out var value) ? value : null); }
/// <summary> /// Gets sequenced JSON from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>JSON content</returns> public Dictionary <string, object> GetSeq(ResourceRef res, CancellationToken ct = default) { // Retrieve response from cache (if available). var key = res.Uri.AbsoluteUri; Response response_cache = null; lock (_lock) if (!TryGetValue(key, out response_cache)) { response_cache = null; } // Get instance source. var response_web = Xml.Response.Get( res: res, ct: ct, previous: response_cache); // Parse instance source JSON. var obj_web = (Dictionary <string, object>)eduJSON.Parser.Parse(response_web.Value, ct); if (response_web.IsFresh) { if (response_cache != null) { try { // Verify sequence. var obj_cache = (Dictionary <string, object>)eduJSON.Parser.Parse(response_cache.Value, ct); bool rollback = false; try { rollback = (uint)eduJSON.Parser.GetValue <int>(obj_cache, "seq") > (uint)eduJSON.Parser.GetValue <int>(obj_web, "seq"); } catch { rollback = true; } if (rollback) { // Sequence rollback detected. Revert to cached version. obj_web = obj_cache; response_web = response_cache; } } catch { } } // Save response to cache. lock (_lock) this[key] = response_web; } return(obj_web); }
/// <summary> /// Gets sequenced JSON from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>JSON content</returns> public Dictionary <string, object> GetSeq(ResourceRef res, CancellationToken ct = default) { // Retrieve response from cache (if available). var key = res.Uri.AbsoluteUri; Response response_cache = null; lock (_lock) if (!TryGetValue(key, out response_cache)) { response_cache = null; } // Get instance source. var response_web = Xml.Response.Get( res: res, ct: ct, previous: response_cache); // Parse instance source JSON. var obj_web = (Dictionary <string, object>)eduJSON.Parser.Parse(response_web.Value, ct); if (response_web.IsFresh) { if (response_cache != null) { // Verify version. var obj_cache = (Dictionary <string, object>)eduJSON.Parser.Parse(response_cache.Value, ct); if (eduJSON.Parser.GetValue(obj_cache, "v", out int v_cache)) { if (!eduJSON.Parser.GetValue(obj_web, "v", out int v_web) || v_web <= v_cache) { // Version rollback detected. Revert to cached version. obj_web = obj_cache; response_web = response_cache; } } } // Save response to cache. lock (_lock) this[key] = response_web; } return(obj_web); }
/// <summary> /// Gets sequenced JSON from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>JSON content</returns> public Dictionary <string, object> GetSeq(ResourceRef res, CancellationToken ct = default) { // Retrieve response from cache. var cachedResponse = GetSeqFromCache(res); // Get JSON. var webResponse = Response.Get( res: res, ct: ct, previous: cachedResponse); // Parse JSON. var objWeb = (Dictionary <string, object>)eduJSON.Parser.Parse(webResponse.Value, ct); if (webResponse.IsFresh) { if (cachedResponse != null) { // Verify version. var objCache = (Dictionary <string, object>)eduJSON.Parser.Parse(cachedResponse.Value, ct); if (eduJSON.Parser.GetValue(objCache, "v", out long vCache)) { if (!eduJSON.Parser.GetValue(objWeb, "v", out long vWeb) || vWeb <= vCache) { // Version rollback detected. Revert to cached version. objWeb = objCache; webResponse = cachedResponse; } } } // Save response to cache. lock (Lock) this[res.Uri.AbsoluteUri] = webResponse; } return(objWeb); }
/// <summary> /// Gets UTF-8 text from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="param">Parameters to be sent as <c>application/x-www-form-urlencoded</c> name-value pairs</param> /// <param name="token">OAuth access token</param> /// <param name="responseType">Expected response MIME type</param> /// <param name="previous">Previous content, when refresh is required</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>Content</returns> public static Response Get(ResourceRef res, NameValueCollection param = null, AccessToken token = null, string responseType = "application/json", Response previous = null, CancellationToken ct = default) { // Create request. var request = WebRequest.Create(res.Uri); request.CachePolicy = CachePolicy; request.Proxy = null; if (token != null) { token.AddToRequest(request); } if (request is HttpWebRequest httpRequest) { httpRequest.UserAgent = UserAgent; httpRequest.Accept = responseType; if (previous != null && param != null) { httpRequest.IfModifiedSince = previous.Timestamp; if (previous.ETag != null) { httpRequest.Headers.Add("If-None-Match", previous.ETag); } } } if (param != null) { // Send data. UTF8Encoding utf8 = new UTF8Encoding(); var binBody = Encoding.ASCII.GetBytes(string.Join("&", param.Cast <string>().Select(e => string.Format("{0}={1}", HttpUtility.UrlEncode(e, utf8), HttpUtility.UrlEncode(param[e], utf8))))); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = binBody.Length; try { using (var requestStream = request.GetRequestStream()) requestStream.Write(binBody, 0, binBody.Length, ct); } catch (WebException ex) { throw new AggregateException(Resources.Strings.ErrorUploading, ex.Response is HttpWebResponse ? new WebExceptionEx(ex, ct) : ex); } } ct.ThrowIfCancellationRequested(); // Wait for data to start comming in. WebResponse response; try { response = request.GetResponse(); } catch (WebException ex) { // When the content was not modified, return the previous one. if (ex.Response is HttpWebResponse httpResponse) { if (httpResponse.StatusCode == HttpStatusCode.NotModified) { previous.IsFresh = false; return(previous); } throw new WebExceptionEx(ex, ct); } throw new AggregateException(Resources.Strings.ErrorDownloading, ex); } ct.ThrowIfCancellationRequested(); using (response) { // Read the data. var data = new byte[0]; using (var stream = response.GetResponseStream()) { var buffer = new byte[1048576]; for (; ;) { // Read data chunk. var count = stream.Read(buffer, 0, buffer.Length, ct); if (count == 0) { break; } // Append it to the data. var newData = new byte[data.LongLength + count]; Array.Copy(data, newData, data.LongLength); Array.Copy(buffer, 0, newData, data.LongLength, count); data = newData; } } if (res.PublicKeys != null) { // Generate signature URI. var uriBuilderSig = new UriBuilder(res.Uri); uriBuilderSig.Path += ".minisig"; // Create signature request. request = WebRequest.Create(uriBuilderSig.Uri); request.CachePolicy = CachePolicy; request.Proxy = null; if (token != null) { token.AddToRequest(request); } if (request is HttpWebRequest httpRequestSig) { httpRequestSig.UserAgent = UserAgent; httpRequestSig.Accept = "text/plain"; } // Read the Minisign signature. byte[] signature = null; try { using (var responseSig = request.GetResponse()) using (var streamSig = responseSig.GetResponseStream()) { ct.ThrowIfCancellationRequested(); using (var readerSig = new StreamReader(streamSig)) { foreach (var l in readerSig.ReadToEnd(ct).Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { if (l.Trim().StartsWith($"untrusted comment:")) { continue; } signature = Convert.FromBase64String(l); break; } if (signature == null) { throw new SecurityException(string.Format(Resources.Strings.ErrorInvalidSignature, res.Uri)); } } } } catch (WebException ex) { throw new AggregateException(Resources.Strings.ErrorDownloadingSignature, ex.Response is HttpWebResponse ? new WebExceptionEx(ex, ct) : ex); } ct.ThrowIfCancellationRequested(); // Verify Minisign signature. using (var s = new MemoryStream(signature, false)) using (var r = new BinaryReader(s)) { if (r.ReadChar() != 'E') { throw new ArgumentException(Resources.Strings.ErrorUnsupportedMinisignSignature); } byte[] payload; switch (r.ReadChar()) { case 'd': // PureEdDSA payload = data; break; case 'D': // HashedEdDSA payload = new eduEd25519.BLAKE2b(512).ComputeHash(data); break; default: throw new ArgumentException(Resources.Strings.ErrorUnsupportedMinisignSignature); } ulong keyId = r.ReadUInt64(); if (!res.PublicKeys.ContainsKey(keyId)) { throw new SecurityException(Resources.Strings.ErrorUntrustedMinisignPublicKey); } var sig = new byte[64]; if (r.Read(sig, 0, 64) != 64) { throw new ArgumentException(Resources.Strings.ErrorInvalidMinisignSignature); } using (eduEd25519.ED25519 key = new eduEd25519.ED25519(res.PublicKeys[keyId])) if (!key.VerifyDetached(payload, sig)) { throw new SecurityException(string.Format(Resources.Strings.ErrorInvalidSignature, res.Uri)); } } } return (response is HttpWebResponse webResponse ? new Response() { Value = Encoding.UTF8.GetString(data), Timestamp = DateTime.TryParse(webResponse.GetResponseHeader("Last-Modified"), out var timestamp) ? timestamp : default,
/// <summary> /// Gets UTF-8 text from the given URI. /// </summary> /// <param name="res">URI and public key for signature verification</param> /// <param name="param">Parameters to be sent as <c>application/x-www-form-urlencoded</c> name-value pairs</param> /// <param name="token">OAuth access token</param> /// <param name="responseType">Expected response MIME type</param> /// <param name="previous">Previous content, when refresh is required</param> /// <param name="ct">The token to monitor for cancellation requests</param> /// <returns>Content</returns> public static Response Get(ResourceRef res, NameValueCollection param = null, AccessToken token = null, string responseType = "application/json", Response previous = null, CancellationToken ct = default) { WebRequest request; WebResponse response; var uri = res.Uri; for (var redirectHop = 0; ; redirectHop++) { ct.ThrowIfCancellationRequested(); // Create request. request = CreateRequest(uri, token, responseType); if (request is HttpWebRequest httpRequest) { if (previous != null && param == null) { if (previous.LastModified != DateTimeOffset.MinValue) { httpRequest.IfModifiedSince = previous.LastModified.UtcDateTime; } if (previous.ETag != null) { httpRequest.Headers.Add("If-None-Match", previous.ETag); } } } if (param != null) { // Send data. var utf8 = new UTF8Encoding(); var binBody = Encoding.ASCII.GetBytes(string.Join("&", param.Cast <string>().Select(e => string.Format("{0}={1}", HttpUtility.UrlEncode(e, utf8), HttpUtility.UrlEncode(param[e], utf8))))); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.ContentLength = binBody.Length; try { using (var requestStream = request.GetRequestStream()) requestStream.Write(binBody, 0, binBody.Length, ct); } catch (WebException ex) { throw new AggregateException(Resources.Strings.ErrorUploading, ex.Response is HttpWebResponse ? new WebExceptionEx(ex, ct) : ex); } } ct.ThrowIfCancellationRequested(); // Wait for data to start comming in. try { response = request.GetResponse(); if (response is HttpWebResponse httpResponse) { switch (httpResponse.StatusCode) { case HttpStatusCode.OK: case HttpStatusCode.Created: case HttpStatusCode.Accepted: case HttpStatusCode.NonAuthoritativeInformation: case HttpStatusCode.NoContent: case HttpStatusCode.ResetContent: case HttpStatusCode.PartialContent: break; case HttpStatusCode.MovedPermanently: case HttpStatusCode.TemporaryRedirect: case (HttpStatusCode)308: // Redirect using the same method. if (redirectHop >= (request as HttpWebRequest).MaximumAutomaticRedirections) { throw new HttpTooMayRedirectsException(); } uri = new Uri(uri, httpResponse.GetResponseHeader("Location")); if (uri.Scheme != "https") { throw new HttpRedirectToUnsafeUriException(); } continue; case HttpStatusCode.Found: case HttpStatusCode.SeeOther: // Redirect using GET method. if (redirectHop >= (request as HttpWebRequest).MaximumAutomaticRedirections) { throw new HttpTooMayRedirectsException(); } uri = new Uri(uri, httpResponse.GetResponseHeader("Location")); if (uri.Scheme != "https") { throw new HttpRedirectToUnsafeUriException(); } param = null; continue; case HttpStatusCode.NotModified: // When the content was not modified, return the previous one. previous.IsFresh = false; return(previous); default: throw new NotImplementedException(); } } } catch (WebException ex) { if (ex.Response is HttpWebResponse httpResponse) { throw new WebExceptionEx(ex, ct); } throw new AggregateException(Resources.Strings.ErrorDownloading, ex); } break; } ct.ThrowIfCancellationRequested(); using (response) { // Read the data. var data = Array.Empty <byte>(); try { using (var stream = response.GetResponseStream()) { var buffer = new byte[1048576]; try { for (; ;) { // Read data chunk. var count = stream.Read(buffer, 0, buffer.Length, ct); if (count == 0) { break; } // Append it to the data. var newData = new byte[data.LongLength + count]; Array.Copy(data, newData, data.LongLength); data.Clear(0, data.LongLength); Array.Copy(buffer, 0, newData, data.LongLength, count); data = newData; } } finally { buffer.Clear(0, buffer.Length); } } if (res.PublicKeys != null) { // Generate signature URI. var uriBuilderSig = new UriBuilder(res.Uri); uriBuilderSig.Path += ".minisig"; // Create signature request. request = CreateRequest(uriBuilderSig.Uri, token, "text/plain"); // Read the Minisign signature. byte[] signature = null; try { using (var responseSig = request.GetResponse()) { // When request redirects are disabled, GetResponse() doesn't throw on 3xx status. if (responseSig is HttpWebResponse httpResponseSig && httpResponseSig.StatusCode != HttpStatusCode.OK) { throw new WebException("Response status code not 200", null, WebExceptionStatus.UnknownError, responseSig); } using (var streamSig = responseSig.GetResponseStream()) { ct.ThrowIfCancellationRequested(); using (var readerSig = new StreamReader(streamSig)) { foreach (var l in readerSig.ReadToEnd(ct).Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { if (l.Trim().StartsWith($"untrusted comment:")) { continue; } signature = Convert.FromBase64String(l); break; } if (signature == null) { throw new SecurityException(string.Format(Resources.Strings.ErrorInvalidSignature, res.Uri)); } } } } } catch (WebException ex) { throw new AggregateException(Resources.Strings.ErrorDownloadingSignature, ex.Response is HttpWebResponse ? new WebExceptionEx(ex, ct) : ex); } ct.ThrowIfCancellationRequested(); // Verify Minisign signature. using (var s = new MemoryStream(signature, false)) using (var r = new BinaryReader(s)) { if (r.ReadChar() != 'E') { throw new ArgumentException(Resources.Strings.ErrorUnsupportedMinisignSignature); } var alg = r.ReadChar(); var keyId = r.ReadUInt64(); if (!res.PublicKeys.ContainsKey(keyId)) { throw new SecurityException(Resources.Strings.ErrorUntrustedMinisignPublicKey); } var sig = new byte[64]; if (r.Read(sig, 0, 64) != 64) { throw new ArgumentException(Resources.Strings.ErrorInvalidMinisignSignature); } var key = res.PublicKeys[keyId]; var payload = alg == 'd' && (key.SupportedAlgorithms & MinisignPublicKey.AlgorithmMask.Legacy) != 0 ? data : alg == 'D' && (key.SupportedAlgorithms & MinisignPublicKey.AlgorithmMask.Hashed) != 0 ? new eduLibsodium.BLAKE2b(512).ComputeHash(data) : throw new ArgumentException(Resources.Strings.ErrorUnsupportedMinisignSignature); using (var k = new eduLibsodium.ED25519(key.Value)) if (!k.VerifyDetached(payload, sig)) { throw new SecurityException(string.Format(Resources.Strings.ErrorInvalidSignature, res.Uri)); } } } if (response is HttpWebResponse httpResponse) { var charset = httpResponse.CharacterSet; var encoding = !string.IsNullOrEmpty(charset) ? Encoding.GetEncoding(charset) : Encoding.UTF8; return(new Response() { Value = encoding.GetString(data), // SECURITY: Securely convert data to a SecureString ContentType = httpResponse.ContentType, Date = DateTimeOffset.TryParse(httpResponse.GetResponseHeader("Date"), out var date) ? date : DateTimeOffset.Now, Expires = DateTimeOffset.TryParse(httpResponse.GetResponseHeader("Expires"), out var expires) ? expires : DateTimeOffset.MaxValue, LastModified = DateTimeOffset.TryParse(httpResponse.GetResponseHeader("Last-Modified"), out var lastModified) ? lastModified : DateTimeOffset.MinValue, ETag = httpResponse.GetResponseHeader("ETag"), Authorized = token != null ? token.Authorized : DateTimeOffset.MinValue, IsFresh = true }); }