/// <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);
        }
Пример #2
0
        /// <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);
        }
Пример #3
0
        /// <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);
        }
Пример #4
0
        /// <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);
        }
Пример #5
0
        /// <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);
        }
Пример #6
0
        /// <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,
Пример #7
0
        /// <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
                        });
                    }