esmaspäev, 1. juuli 2019

Java streams SQL laadne GROUP BY

Kui vaja teha Java mingi class listidega SQL laadne GROUP BY arvutus

InventoryPositionsViewRate on algne klass mis on vaja summeerida tInventorySummaryViewRate listiks

private void calculateSummary(final List<InventoryPositionsViewRate> ipwr) {
        // Calculates sum GROUP BY branch, currency
        this.listInventorySummaryViewRate = ipwr.parallelStream()
                .collect(Collectors.groupingBy(Function.identity(),
                        () -> new TreeMap<>(Comparator.<InventoryPositionsViewRate, String> comparing(InventoryPositionsViewRate::getBranchDisplayName)
                                .thenComparing(InventoryPositionsViewRate::getCurrencyCode)
                                .thenComparing(InventoryPositionsViewRate::getRate)), //getRate is nessesary for localAmount calculating
                        Collectors.summingDouble(foo -> foo.getAmount().doubleValue())))
                .entrySet().stream()
                .map(iswr -> new InventoryPositionsSummaryRate(iswr.getKey().getBranchDisplayName(),
                        iswr.getKey().getCurrencyCode(), BigDecimal.valueOf(iswr.getValue()), iswr.getKey().getRate()))
                .collect(Collectors.toList());
    }

esmaspäev, 25. märts 2019

Viga 403.16 Forbidden ID-kaardiga autentimisel

Kui kasutatakse rakenduses ID-kaardiga autentmist, ehk ASP.NET koodis on

HttpClientCertificate sert = Request.ClientCertificate;
if (!sert.IsPresent)
...

ning oma arust on veebilehe autentimine õigesti seadistatud (IIS, WebSite - SSL Settings - Client Certificates (Accept)

võib ikkagi juhtuda, et tuleb sirviku ekraanile viga 403 Forbidden

Et asjale pihta saada, säti veebelehel peale Failed Request Tracing



seadistada sobivad märkeruudud ja jätta meelde kataloog, kuhu logifailid tekitatakse (XML fail on veateade ise ja XSL on selleks, et Internet Exploreriga viga vaadata)

Kui veateade sisaldab endast 403.16 ja kirjeldust A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider. (0x800b0109)



siis on midagi valesti Windows Serveri sertifikaatides, mis alates Windows Server 2012 nõutakse, et juurhoidlas oleks ainult self-signed sertifikaadid. Kui seal on mõni mittesobiv, siis tulemuseks ongi 403 Forbidden

PowerShelli käsuga saab need sertifikaadid ülesse leida
Get-Childitem cert:\LocalMachine\root -Recurse |   Where-Object {$_.Issuer -ne $_.Subject}


edasi siis juba vaadata, mida nende sertifikaatidega teha, kas kustutada või liigutada vahepealsesse sertifikaadi hoidlasse



Get-Childitem cert:\LocalMachine\root -Recurse |  Where-Object {$_.Issuer -ne $_.Subject} | 
    Move-Item -Destination Cert:\LocalMachine\CA








reede, 15. veebruar 2019

Sertifikaatide tegemine Telia XTee keskkonnaga liitumiseks

XTeele andmeid edastava serveri ühendamiseks Telia turvaserveriga on vaja genereerida sertifikaadid ja parooliga võti, seda saab teha OPENSSL utiliidi kaudu

Windows keskkonnas näiteks https://slproweb.com/products/Win32OpenSSL.html

Käsud on järgmised

Sertifikaadi genereerimine

openssl req -new -newkey rsa:4096 -x509 -sha256 -days 1826 -nodes -out TartuYliopilaskyla.crt -keyout TartuYliopilaskyla.key

-days 1826  määrab kaua päev sertifikaat kehtib
Common Name pane rakenduse URL aadress


TartuYliopilaskyla.crt on siis serveri sertifikaat, mis tuleb saata Teliasse ja paigaldada ka Windows Serveri Trusted Root sertifikaadihoidlasse

Privaatvõtme tegemine, parool jäta meelde, seda läheb importmisel vaja

openssl pkcs12 -export -out personalprivat.pfx -inkey TartuYliopilaskyla.key -in TartuYliopilaskyla.crt



personalprivat.pfx on nüüd parooliga sertifikaat mis paigaldada Windows Serveri Personal sertifikaadihoidlasse. Paigaldamisel küsitakse parooli, mis genereerimisel sai sisestatud.



Kui kõik sobib siis Personal sertifikaadihoidlas olev Certification Path peab olema vigadeta



Selleks, et C# meetodid saaksid seda sertifikaati kasutada tuleb rakenduste kontodele anda ka ligipääsu õigused.

Õiguste andmine rakenduste kontodele


teisipäev, 12. veebruar 2019

Lehekülgede pagineerimine ORACLE-s ühe SELECT lause kasutamisega ainult


Eriti REST rakenduste puhul tuleb sageli pagineerimine teha andmebaasi poolel.

Läbi tuleb teha selleks kaks etappi
1. Saada teade kirjete koguarv
2. Võtta vajalik vahemik välja

ORACLE WITH konstruktsiooni tehes saab selle tehe ühe SELECT lausega ainult, mis juhtudel, kui SELECT lause ise võtab kaua aega annab märgatava ajalise kokkuhoiu

Näide, kus annad ette lehekülje suuruse ja mitmendat lehekülge tahad

SET SERVEROUTPUT ON
DECLARE i_page_size INTEGER;  --lehekülje suurus
i_page_no INTEGER; --mitme lehekülg kuvada
BEGIN
i_page_size:=5;
i_page_no:=2;
FOR ss IN (
 WITH Tu AS
        (  SELECT ROWNUM AS jrk, dde.ID, dde.SUBJECT_ID, dde.OCCUP_NAME
                , CEIL((ROWNUM)/i_page_size) AS PAGE_NUMBER --arvutab lehekülje numbri
            FROM
            (  SELECT ID, SUBJECT_ID, OCCUP_NAME FROM SUB_EMPLOYER ORDER BY OCCUP_NAME DESC ) dde
     )       
     SELECT Tu.jrk, Tu.ID, Tu.SUBJECT_ID, Tu.OCCUP_NAME,Tu.PAGE_NUMBER
       ,(SELECT MAX(Tu.jrk) FROM Tu ) AS kokku_kirjeid
       FROM Tu
         WHERE Tu.PAGE_NUMBER = i_page_no  --pagineerimine
         ORDER BY Tu.jrk)
LOOP
    DBMS_OUTPUT.PUT_LINE ( SS.JRK || ' ' || ss.OCCUP_NAME || ' ' || ss.SUBJECT_ID  || ' ' || ss.kokku_kirjeid);
END LOOP;
END;

WITH konstruktsiooni osa, saab kätte kirje järjekorra numbri ja CEIL funktsiooniga arvutab lehekülje numbri

ROWNUM AS jrk, CEIL((ROWNUM)/i_page_size) AS PAGE_NUMBER
      
 hilisemas välises päringus saab eraldi tulbast kätte ka kirjete koguarvu
(SELECT MAX(Tu.jrk) FROM Tu ) AS kokku_kirjeid



Mõnikord vaja saada lisaks eraldi teada ka kirjete ja lehekülgede koguarv siis kasutab CURSOR-i kuhu lisab ette esimeseks lehekülgede ja ridade koguarvuga rea ning FETCH-ib selle välja enne tagastamist. Esimese rea väljatõmbamisel saad lehekülgede ja kirjete koguarvu teada
o_totalPages:=nvl(ceil(o_totalRows/i_page_size),0); --arvutame lehekülgede arvu
o_totalRows:=nvl(o_totalRows,0); --nvl et saaks ridade puudumisel number 0-i välja



SET SERVEROUTPUT ON
DECLARE i_page_size INTEGER;  --lehekülje suurus
i_page_no INTEGER; --mitmes lehekülg kuvada
o_cursor sys_refcursor;  --siia paneme tulemused
o_totalPages INTEGER; --mitu lehekülge kokku
o_totalRows INTEGER; --mitu rida kokku

v_int integer; --abimuutuja
v_int2 integer; --abimuutuja
v_lehekulg integer; --abimuutuja
v_subject_id varchar2(8000); --abimuutuja
v_occup_name varchar2(8000); --abimuutuja
BEGIN
i_page_size:=5;
i_page_no:=2;

open o_cursor for
 WITH Tu AS
        ( 
        SELECT dde2.jrk, dde2.ID, dde2.SUBJECT_ID, dde2.OCCUP_NAME, dde2.PAGE_NUMBER
        FROM (
        SELECT ROWNUM AS jrk, dde.ID, dde.SUBJECT_ID, dde.OCCUP_NAME
                , CEIL((ROWNUM)/i_page_size) AS PAGE_NUMBER --arvutab lehekülje numbri
            FROM
            (  SELECT ID, SUBJECT_ID, OCCUP_NAME FROM SUB_EMPLOYER ORDER BY OCCUP_NAME DESC ) dde ) dde2
     )       
     SELECT Tu.jrk, Tu.ID, Tu.SUBJECT_ID, Tu.OCCUP_NAME,Tu.PAGE_NUMBER
       ,(SELECT MAX(Tu.jrk) FROM Tu ) AS kokku_kirjeid
       FROM Tu
         WHERE Tu.PAGE_NUMBER = i_page_no  --pagineerimine
       
    UNION --tekitame juurde esimese rea, sellekaudu saab fetch ridade koguarvu teada väljastuseks
        SELECT 0 as jrk, Tu.ID, Tu.SUBJECT_ID, Tu.OCCUP_NAME,Tu.PAGE_NUMBER
       ,(SELECT MAX(Tu.jrk) FROM Tu ) AS kokku_kirjeid FROM Tu WHERE Tu.jrk =1
         ORDER BY 1;  --et 0 as jrk rida esimeseks tuleks, mille FETCH-iga välja võtame
        
    --Esimese rea kursorist võib ära raisata, küsib sealt lehekülgede koguarvu ja kirjete koguarvu
     FETCH o_cursor INTO v_int, v_int2, v_subject_id, v_occup_name, v_lehekulg,o_totalRows;
             o_totalPages:=nvl(ceil(o_totalRows/i_page_size),0); --arvutame lehekülgede arvu
             o_totalRows:=nvl(o_totalRows,0); --nvl et saaks ridade puudumisel number 0-i välja
     IF o_totalRows=0 THEN   --anname tühja kursori välja, kui kirjeid üldse pole, tuleb viga ORA-01002: fetch out of sequence
        CLOSE o_cursor;
        OPEN o_cursor FOR
         SELECT 0 AS jrk , 0 AS ID, '' AS SUBJECT_ID, '' AS OCCUP_NAME, 0 AS PAGE_NUMBER, 0 AS kokku_kirjeid FROM dual;
     END IF;
     DBMS_OUTPUT.PUT_LINE ( 'Kokku kirjeid: ' || o_totalRows  || '   Kokku lehekülgi: ' || o_totalPages);
    
    
     LOOP
          FETCH o_cursor INTO  v_int, v_int2, v_subject_id, v_occup_name, v_lehekulg,o_totalRows;
             EXIT WHEN o_cursor%NOTFOUND;
          DBMS_OUTPUT.PUT_LINE ('Jrk: ' ||  v_int || ' ' || v_occup_name || ' ' || v_subject_id);
     END LOOP;
    
END;

Konstruktsioon 
.. UNION .. WHERE Tu.jrk =1 .. ORDER BY 1  teeb selle, et "tühi rida" mille võib ära visata tekib CURSOR-isse esimeseks, kust loeb välja kirjete ja lehekülgede koguarvu.

Kui kirjeid pole IF o_totalRows=0 THEN siis tekitab tühja üherealise CURSOR-i REST kliendi jaoks juhuks kui REST kliendile ei meeldi, et andmebaasist mitte kui midagi ei tagastata.

esmaspäev, 11. veebruar 2019

Validate of viewstate MAC failed viga IIS HTTPS ühenduse korral

Kui oled IISi peale paigaldanud veebirakenduse ja seadistanud ta ka HTTPS protokolli kasutama siis võib käivitamisel ilmuda viga koos soovitustes antud lingiga


Üks asi mida veel vaadata on web.config failis httpCookies seadistus, millele saab lisada turvalisust tõstvaid atribuute requireSSL="true" ja domain="www.veebileht.ee".
Jälgida tuleb, et domain parameeter langeks kokku veebilehe URL aadressiga. Selle kaudu kontrollitakse, et ViewState tuleb õigelt veebilahelt

pühapäev, 10. veebruar 2019

ASP.NET digiallkirjastamine IIS veebiserveris

Mõnikord siis vaja ka ASP.NET veebirakendusse digiallkirjastamine lisada, üks võimalus siis nii, et teed digidoc4j põhjal Java Spring Boot rakenduse mille käivitad Windows Serveris kui Wrapper teenuse.

Sellest juhendist Windows Service Wrapper täitsa töötab
https://www.baeldung.com/spring-boot-app-as-a-service

Idee siis selline, et näidisrakendust https://github.com/rvillido/digidoc4j-hwcrypto-demo
on muudetud nii, et Java osas toimub signeerimine ja valideerimine aga andmete salvestamine on Java Spring Boot rakendusest välja toodud ja tehakse ASP.NET ja MS SQL SERVER-i sees
https://github.com/open-eid/digidoc4j/wiki/Examples-of-using-it

Java Spring Boot rakendus asub siin, kõigepealt tuleb serverisse Java 1.8 paigaldada
https://github.com/kuidokylm/KampusBdoc

Kui saad selle Windows Service Wrapperi abil paigaldatud siis ava Windows Serveris sirvik ja vaata, mis seal http://localhost:8083 vastu vaatab, peaks tulema selline pilt


Java Spring Boot töötab GET ja POST päringute töötlemisega URL-i pihta, ehk serveri peal
sirviku http://localhost:8083/port päring peaks andma sellise vastuse


Kõik test URL-id leiab  Java @RestController public class TestController-ist


Digiallkirjastamise jaoks tuleb sõlmida SK ID Solutions leping Ajatempliteenuse kehtivuskinnituse kasutamiseks, kui see on tehtud ja IP aadress avatud siis serveri peal http://localhost:8083/refreshtsl


peab tagastama ok, ehk tehakse TSL(trusted sources list) nimekirja värskendus. Edasi on juba ASP.NET koodi osa, ehk lisad projekti ASMX veebiteenuse, näites on nimi bdoc.asmx

ja tema code-behind koodi päisesse

[System.Web.Script.Services.ScriptService]
public class bdoc : System.Web.Services.WebService

ehk lubad JavaScriptil/jQueryl ASP.NET veebiteenusega suhelda.

ASMX veebiteenuse meetod Port, mis tootab nii, et ASP.NET koodis veebilehel pöördutakse jQuery kaudu ASMX meetodi Port poole, mis teeb päringu Java Spring Boot Service Wrapperisse http://localhost:8083/port, saab sealt vastuse ja tagastab tagasi veebilehele

[WebMethod]
[ScriptMethod(UseHttpGet = true)]
    public void Port()
    {
        string responseFromServer = "";
        string url = "http://localhost:8083/port";     
        try
        {
            WebRequest request = WebRequest.Create(url);
            ((HttpWebRequest)request).UserAgent = "Kampus Web";
            WebResponse response = request.GetResponse();
            Stream dataStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(dataStream);
            responseFromServer = reader.ReadToEnd();

            reader.Close();
            dataStream.Close();
            response.Close();

        }
        catch (WebException ex)
        {
            responseFromServer = ex.Message;
            if (ex.InnerException != null)
            {
                responseFromServer += (" " + ex.InnerException.Message);
            }
        }
        catch (SystemException ex)
        {
            responseFromServer = ex.Message;
        }
        Digest vastus = new Digest();
        vastus.result = responseFromServer;
        vastus.hex = responseFromServer;
        responseFromServer = Newtonsoft.Json.JsonConvert.SerializeObject(vastus);
        Context.Response.Clear();
        Context.Response.ContentType = "application/json;charset=UTF-8";  //see peab olema, muidu ei hakka jquery front-endis tööle, vajab JSON formaati
        Context.Response.AddHeader("content-length", responseFromServer.Length.ToString());
        Context.Response.Flush();
        Context.Response.Output.Write(responseFromServer);
    }

jQuery väljakutse ASPX lehe peal olevast HTML ButtonPort input nupust

input type="button" id="ButtonPort" class="actionButton" value="Kontrolli ASMX BDOC"

Päring töötab nii, ehk

1. Sirvikust jQuery pöördumine ASP.NET /bdoc.asmx/Port meetodisse
2. ASMX meetod Port teeb serveris päringu Java Spring Boot http://localhost:8083/port aadressil
3. Saadud vastuse tagastab ASMX meetod Port sirviku jQueryle


$("#ButtonPort").on("click", function () {
 console.log("Port ");                 
 $.get("/bdoc.asmx/Port", function (data, status) {
 console.log("Port " + data.hex);
 $("#<%=LabelBdoc.ClientID %>").text(status);
 alert("Data: " + data.hex + "\nStatus: " + status);
});
});



Sama Java Spring Boot http://localhost:8083/port väljakutse serveri poolel, ehk ASP.NET code-behind nupu Click meetod kus serveri back-end pöördub otse Java Spring Boot Windows Service wrapperi poole

protected void ButtonBDOC_Click(object sender, EventArgs e)
    {
        string url = "http://localhost:8083/port";
        this.LabelBdoc.Visible = true;
        //https://docs.microsoft.com/en-us/dotnet/framework/network-programming/how-to-request-data-using-the-webrequest-class
        try
        {
            WebRequest request = WebRequest.Create(url);
            ((HttpWebRequest)request).UserAgent = "Kampus WebAdmin ButtonBDOC_Click";
            WebResponse response = request.GetResponse();
            Stream dataStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(dataStream);
            string responseFromServer = reader.ReadToEnd();
            this.LabelBdoc.Text = responseFromServer;
            reader.Close();
            dataStream.Close();
            response.Close();

        }
        catch (WebException ex)
        {
            this.CustomValidator1.ErrorMessage = ex.Message;
            this.CustomValidator1.IsValid = false;
            if (ex.InnerException != null)
            {
                this.LabelBdoc.Text = url + " " + ex.InnerException.Message;
            }
        }
        catch (SystemException ex)
        {
            this.CustomValidator1.ErrorMessage = url + " " + ex.Message;
            this.CustomValidator1.IsValid = false;
        }
    }


Kaks võimalust, kuidas Java Spring Boot Windows Service wrapperi kaudu suhelda, kas front-endis kus pöördutakse läbi ASMX veebimeetodi või server-side kus minnakse otse.

Kui on vaja ASP.NET koodist ASMX veebimeetodile Session -i kaudu väärtust edasi anda
siis ASMX meetodi päisesse lisad [WebMethod(EnableSession = true)]

[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public void generateFileHash(string certInHex)
{
int failiId = Convert.ToInt32(HttpContext.Current.Session["bdocid"].ToString());
}

nii saad ASP.NET code-behind seatud Session muutuja väärtuse kätte ASMX veebimeetodis

Kui näiteks on GridView nimekiri failidest mida vaja digiallkirjastada




asp:Button ID="BtnSigneeri"  Text="Digiallkirjasta leping" CausesValidation="false" CommandName="Signeeri" CommandArgument='<%# Eval("ID") %>' runat="server"


protected void GridViewFailid_RowCommand(object sender, GridViewCommandEventArgs e)
    {
        if (e.CommandName.Equals("Signeeri") )
        {
            this.Session["bdocid"] = e.CommandArgument.ToString();  //faili ID
            Button butt = (Button)e.CommandSource;
            if (!this.Page.ClientScript.IsClientScriptBlockRegistered("signeeriDokument()"))
            {
                //kuna oleme updatepaneli sees, tuleb seda ScriptManager.RegisterStartupScript meetodit kasutada
                //kutsume välja selle nupuga seotud javascripti
                ScriptManager.RegisterStartupScript(butt, typeof(Button), butt.ClientID, "signeeriDokument()", true);         
            }
        }

Ülal olevas koodis on ka näidatud, kuidas saab serveri poolt code-behind välja kutsuda JavaScripti meetodit
ScriptManager.RegisterStartupScript(butt, typeof(Button), butt.ClientID, "signeeriDokument()", true);

javascripti meetod oma korda kutsub välja jQuery funktsioonide promised

function signeeriDokument() {
    console.log("signeeriDokument klikiti");
    signfile();
}

signfile() on jQuery funktsioonide komplekt promiseid, kus post funktsiooni sees url: "/bdoc.asmx/" + url määratakse ära väljakutsutav ASMX veebiteenuse meetod

$(function () {

    post = function (url, data) {
        console.log("post query:" + url);
        return new Promise(function (resolve, reject) {
            $.ajax({
                url: "/bdoc.asmx/" + url,
                type: "POST",
                data: data
            }).done(function (data) {
                console.log("post done data.result " + data.result);
                if (data.result !== "ok") {
                    $("#LabelBdoc").text(data.result);
                    reject(Error(data.result))
                } else {
                    resolve(data);
                }
            }).fail(function () {
                reject(Error("Post operation failed"));
            });
        });
    };


    //olemasolevalt konteinerilt räsi küsimine
    getContainerToSign = function (certInHex) {
        console.log("getContainerToSign " + certInHex);
        return post("getContainerToSign", { certInHex: certInHex })
    };

    fetchFileHash = function (certInHex) {
        console.log("generateFileHash ");
        return post("generateFileHash", { certInHex: certInHex })
    };

    createContainer = function (signatureInHex) {
        console.log("createContainer " + signatureInHex);
        return post("createContainer", { signatureInHex: signatureInHex });
    };

    addSignToContainer = function (signatureInHex) {
        console.log("addSignToContainer " + signatureInHex);
        return post("addSignToContainer", { signatureInHex: signatureInHex });
    };


    signfile = function () {
        var cert;
        naitanupp("LabelBdoc");
        $("#LabelBdoc").text("Loen ID-kaardilt sertifikaati");
        console.log("enne signfile window.hwcrypto.getCertificate");
        window.hwcrypto.getCertificate({ lang: 'en' }).then(function (certificate) {
            cert = certificate;
            $("#LabelBdoc").text("Koostan räsi");
            console.log("getCertificate päring ");
            return fetchFileHash(certificate.hex);
        }).then(function (digest) {              
            console.log("digest " + digest.hex);
            $("#LabelBdoc").text("Signeerin");
            return window.hwcrypto.sign(cert, { type: 'SHA-256', hex: digest.hex }, { lang: 'en' });
        }).then(function (signature) {
            console.log("createcontainer");
            $("#LabelBdoc").text("Loon konteinerit");
            return createContainer(signature.hex);
        }).then(function (result) {
            console.log("container is ready for download " + result.result);                         
            $("#LabelBdoc").text(result.result);              
            window.location = window.location;
        }).catch(err => {
            $("#LabelBdoc").text(err);              
        });
    };
});

ASP.NET Labelil nimega LabelBdoc peab ClientIDMode="Static" olema määratud, et tema ID ei muutuks kompileerimise käigus jQuery kirjutab sinna sisse infot protsessi edenemise kohta $("#LabelBdoc").text("Signeerin");

asp:Label ID="LabelBdoc" CssClass="globalNote" ClientIDMode="Static" runat="server"

Kogu protsessi juhib jQuery signfile funktsioon, küsib ID kaardilt sertifikaadi, käib ASMX veebimeetoditelt küsimas faili räsi, signeerib selle ja lõpuks saadab päringuga andmed konteineri tegemiseks.

ASMX meetodid mis käivad JavaSpring Boot Windows Service wrapperisse andmeid saatmas ja salvestavad vastuvõetu SQL SERVER-i tabelitesse


[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
    public void generateFileHash(string certInHex)
    {
        Digest vastus = new Digest();
        byte[] byteArray = null;
        string fileName = "";
        int failiId =  Convert.ToInt32(HttpContext.Current.Session["bdocid"].ToString());     
        string contype = "";
        try
        {
            String connection = Configuration.ConnectionString;
            using (SqlConnection konn = new SqlConnection(connection))
            {
                SqlCommand komm = new SqlCommand("[dbo].[CONTRACT_FILE_KUIDO_S]", konn);
                komm.CommandType = CommandType.StoredProcedure;
                komm.Parameters.AddWithValue("@fileid", failiId);
                konn.Open();
                SqlDataReader red = komm.ExecuteReader();
                while (red.Read())
                {
                    fileName = red["FILE_NAME"].ToString();
                    contype = red["ADVERTISING_FILE_CONTENT_TYPE"].ToString();
                    byteArray = (byte[])red["FILE_CONTENT"];
                    break;
                }
                red.Close();
                konn.Close();
            }
            string requestURL = "http://localhost:8083/uploadforhash";
            WebClient wc = new WebClient();
            Dictionary<string, object> postParameters = new Dictionary<string, object>();
            // Add your parameters here
            postParameters.Add("file", new BdocUpload.FileParameter(byteArray, Path.GetFileName(fileName), contype));
            postParameters.Add("certInHex", certInHex);
            string userAgent = "KAMPUS generateFileHash";
            HttpWebResponse webResponse = BdocUpload.MultipartFormPost(requestURL, userAgent, postParameters, null, null);
            // Process response
            StreamReader responseReader = new StreamReader(webResponse.GetResponseStream());
            string responseFromServer = responseReader.ReadToEnd();
            webResponse.Close();
            DigestObject djson = JsonConvert.DeserializeObject<DigestObject>(responseFromServer);
            vastus.hex = djson.hex;
            vastus.result = djson.result;
            using (SqlConnection konn = new SqlConnection(connection)) //salvestame Conteineri sisu ajutiselt ära
            {
                SqlCommand komm = new SqlCommand("DBO.CONTRACT_FILE_BDOC_TEMP_U", konn);
                komm.CommandType = CommandType.StoredProcedure;
                komm.Parameters.AddWithValue("@fileid", Convert.ToInt32(this.Session["bdocid"].ToString()));
                komm.Parameters.AddWithValue("@tempdatatosign", djson.datatosign);
                komm.Parameters.AddWithValue("@tempcontainersisu", djson.container);
                konn.Open();
                komm.ExecuteNonQuery();
                konn.Close();
            }
        }
        catch (System.Web.HttpException ex)
        {
            vastus.result = ex.Message;
        }
        catch (SystemException ex)
        {
            vastus.result = ex.Message;
        }
        string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(vastus);  //teeme vastuse json stringiks

        Context.Response.Clear();
        Context.Response.ContentType = "application/json;charset=UTF-8";  //see peab olema, muidu ei hakka jquery front-endis tööle, vajab JSON formaati
        Context.Response.AddHeader("content-length", jsonString.Length.ToString());
        Context.Response.Flush();
        Context.Response.Output.Write(jsonString);
    }

    [WebMethod(EnableSession = true)]
    [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
    public void createContainer(string signatureInHex)
    {
        int failiId = Convert.ToInt32(HttpContext.Current.Session["bdocid"].ToString());
     
        byte[] datatosign = null;
        byte[] container = null;
        String connection = Configuration.ConnectionString;
        using (SqlConnection konn = new SqlConnection(connection))
        {
            SqlCommand komm = new SqlCommand("DBO.CONTRACT_FILE_KUIDO_S", konn);
            komm.CommandType = CommandType.StoredProcedure;
            komm.Parameters.AddWithValue("@fileid", failiId);
            konn.Open();
            SqlDataReader red = komm.ExecuteReader();
            while (red.Read())
            {
                container = (byte[])red["TEMPCONTAINERSISU"];
                datatosign = (byte[])red["TEMPDATATOSIGN"];
                break;
            }
            red.Close();
            konn.Close();
        }
        Digest vastus = new Digest();
        string requestURL = "http://localhost:8083/createContainer";
        WebClient wc = new WebClient();
        Dictionary<string, object> postParameters = new Dictionary<string, object>();
        // Add your parameters here  "tempcontainer", "bytearray" on koha täiteks
        postParameters.Add("file", new BdocUpload.FileParameter(container, "tempcontainer", "bytearray"));
        postParameters.Add("dfile", new BdocUpload.FileParameter(datatosign, "tempdatatosign", "bytearray"));
        postParameters.Add("signatureInHex", signatureInHex);
        string userAgent = "KAMPUS createContainer";
        HttpWebResponse webResponse = BdocUpload.MultipartFormPost(requestURL, userAgent, postParameters, null, null);
        // Process response
        StreamReader responseReader = new StreamReader(webResponse.GetResponseStream());
        string responseFromServer = responseReader.ReadToEnd();
        webResponse.Close();
        DigestObject djson = JsonConvert.DeserializeObject<DigestObject>(responseFromServer);
        vastus.result = djson.result;
        try
        {
            using (SqlConnection konn = new SqlConnection(connection)) //salvestame BDOC Conteineri sisu
            {
                SqlCommand komm = new SqlCommand("DBO.CONTRACT_FILE_BDOC_U", konn);
                komm.CommandType = CommandType.StoredProcedure;
                komm.Parameters.AddWithValue("@fileid", Convert.ToInt32(failiId));
                komm.Parameters.AddWithValue("@bdocfailisisutyyp", djson.hex);
                komm.Parameters.AddWithValue("@bdocfailisisu", djson.container);
                //komm.Parameters.AddWithValue("@tempcontainersisu", djson.datatosign);
                konn.Open();
                komm.ExecuteNonQuery();
                konn.Close();
            }
        }
        catch (SystemException ex)
        {
            vastus.result = ex.Message;
        }
        string jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(vastus);  //teeme vastuse json stringiks
        Context.Response.Clear();
        Context.Response.ContentType = "application/json;charset=UTF-8";  //see peab olema, muidu ei hakka jquery front-endis tööle, vajab JSON formaati
        Context.Response.AddHeader("content-length", jsonString.Length.ToString());
        Context.Response.Flush();
        Context.Response.Output.Write(jsonString);
    }

Java Spring Boot @RestController public class SigningController teeb ära digiallkirjastamise osad.


ASMX saadab faile Java Spring Boot Service Wrapperile multipart form data-na
https://www.c-sharpcorner.com/article/upload-any-file-using-http-post-multipart-form-data/

Selleks, et sirvikus jQuery saaks üldse teha päringuid ASMX veebimeetodite vastu tuleb web.config failis lubada vastavad veebiteenuste protokollid.



JSON andmevahtuseks ASMX ja Java Spring Booti vahel tuleb Java Spring Boot klassid kloonida ASP.NET rakendusse, Newtonsoft.Json package kasutamiseks

public class Signatuur
{
    public Signatuur()
    {
    }
    public string SubjectName { get; set; }
    public DateTime ClaimedSigningTime { get; set; }
    public String issuerName { get; set; }

    public String errors { get; set; }
}

public class Signatuurid
{
    public Signatuurid()
    {
    }
    public string result { get; set; }
    public List<Signatuur> signatuurid { get; set; }
}


public class DigestObject
{
    public DigestObject()
    {
    }

    public string result { get; set; }
    public string hex { get; set; }
    public byte[] datatosign { get; set; }
    public byte[] container { get; set; }
}

reede, 25. jaanuar 2019

Windows Serveri seadistamine Telia X-Tee turvaserverisse SOAP andmete saatmiseks C# veebipäringutega


Kui nüüd on ette tulnud infosüsteemi liidestus X-Teega ja vaja hakata andmeid vahetama läbi Telia XTee turvaserveri tuleb Windows Serveri seadistuses teha serveri poolse ASP.NET koodi jaoks järgmist.

Kontrollida, et serveris oleks TLS 1.2 protokoll lubatud, Telia XTee nõuab TLS 1.2 olemasolu ja ilma selleta ühendust ei tehta. Vastavad TLS 1.2 registrivõtmed DisabledByDefault(0) ja Enabled(1) lisada
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/SecurityProviders/SCHANNEL/Protocols kataloogi. Sõltuvalt Windows Serveri versionist võib TLS 1.2 olla juba vaikimisi lubatud.



Et kliendi sertifikaadiga autentimine toimiks tuleb võimaldada FIPS, ehk Group Policy alt seadistada
lubatuks Use FIPS compliant algorithms for encryption, hashing, and signing seadistus



Edasi tuleb genereerida avaliku ja privaatvõtme paar ning privaatvõti laadida Windows Serveri sertifikaadihoidlasse Local Computer / Personal

Kuidas genereerida sertifikaate

Selleks, et C# kood sertifikaadiga autentida saaks kas siis .NET rakendusest või SQL SERVER CLR C# meetodiga, peab vastaval kontol, mille all rakendus töötab olema ligipääs sertifikaadihoidlasse laetud privaatvõtmele. Juurdepääsu saab anda ainult Local Computer / Personal hoidlas olevatele privaatvõtmega sertifikaatidele

Local Computer / Personal sertifikaadihoidla avamiseks tuleb C# kasutada StoreName.My parameetrit

X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);


Hoidlast räsi järgi otsimiseks X509FindType.FindByThumbprint seadistust. Privaatvõtme serdi räsi saab teada meetodiga X509Certificate2.GetSerialNumberString();

Koodijupp, mis SQL SERVER CLR C# funktsioonina on realiseeritud, selleks, et TLS 1.2 töötaks peab projektis olema kasutatud vähemalt .NET Target Frameworki versioon 4.5 või kõrgem

TLS 1.2 otsene rakendamine ServicePointManager-i kasutades määratakse
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

TLS 1.2 ühendust saab luua kas System.Net.Http.HttpClient või System.Net.Security.SslStream klasse kasutades


System.Net.Http.HttpClient näide

    /// <summary>
    ///   Telia XTee turvaserveri kaudu kliendi sertifikaadiga TLS 1.2 protokolli abil SOAP päring
    /// </summary>
    /// <param name="Url">XTee turvaserveri https URL</param>
    /// <param name="envelope">SOAP päringu XML sisu stringina</param>
    /// <param name="serthash">Local Computer / Personal sertifikaadihoidlas oleva sertifikaadi räsi</param>
    /// <returns></returns>
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlChars XTeeSOAPRequest(SqlString Url, SqlString envelope, SqlString serthash)
    {
        XmlDocument soapXml = new XmlDocument();
        try
        {
            soapXml.LoadXml((string)envelope);
        }
        catch (SystemException ex)
        {
            throw new SystemException("soapXml.LoadXml ERROR:"+ex.Message+"  SISU:"+(string)envelope);
        }
        string soapResult = "";

     
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
     
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create((string)Url);
     
        string chash = (string)serthash;
        if (!String.IsNullOrEmpty(chash) )
        {
         
            X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            certStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

         
            X509Certificate2Collection serts = certStore.Certificates.Find(X509FindType.FindByThumbprint, chash, true);
            if (serts.Count == 0)
            {
                serts = certStore.Certificates.Find(X509FindType.FindByThumbprint, chash, false);
            }
            if (serts.Count != 1)
            {
                throw new SystemException("Sertifikaati räsiga " + chash + "  ei ole hoidlas Local Computer - Personal");
            }
            X509Certificate2 cert = serts[0];
         
            certStore.Close();
            webRequest.ClientCertificates.Clear();
            webRequest.ClientCertificates.Add(cert);
            if (webRequest.ClientCertificates.Count != 1)
            {
                throw new SystemException("HttpWebRequest sertifikaatide tõrge, vale kogus " + cert.IssuerName.Name);
            }

        }
     
        webRequest.ContentType = "text/xml;charset=\"utf-8\"";
        webRequest.Accept = "text/xml";
        webRequest.Method = "POST";
        webRequest.UserAgent = "KAMPUS";
        using (Stream stream = webRequest.GetRequestStream())
        {
            soapXml.Save(stream);
        }

        using (WebResponse response = webRequest.GetResponse())
        {
            using (StreamReader rd = new StreamReader(response.GetResponseStream()))
            {
                soapResult = rd.ReadToEnd();
             
            }
        }

        SqlChars sd = new SqlChars(((SqlString)soapResult));
        return sd; //siin on vastus, SOAP XML
    }

Rakenduse kontole tuleb anda ligipääs privaatvõtmele, Local Computer / Personal sertifikaadihoidlas tuleb vastava privaatvõtme serdi peale teha hiire paremklõps ja valida



Ning seada Read õigused kontole



Mis konto all ASP.NET veebirakendus töötab saab teada C# funktsiooniga
System.Security.Principal.WindowsIdentity.GetCurrent().Name
MS SQL SERVER-i konto Services rakendusest



Veatõrgete jälgimiseks on ASP.NET rakendusel soovitav peale seada System.Diagnostic trace logimine
https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ty48b824(v=vs.100)

Logi veateade The credentials supplied to the package were not recognized  viitab konto õiguste puudumisele privaatvõtmega sertifikaadile ligipääsul

.NET error The request was aborted: Could not create SSL/TLS secure channel viitab privaatvõtmega sertifikaadi puudumisele , FIPS keelamisele või TLS 1.2 mittelubamisele.

Kui ikka segane, pane serveris Wireshark käima


ja jälgi, et nii server teeks Client Certificate küsimise ja klient vastaks ka Certificate vastusega, kui Handshake Protocol Certificates / Certificates Lenght : 0 siis kliendi poolt sertifikaati kaasa ei pandud, kusjuures see kliendi ärakorjamine tehakse Windows Serveri poolt ilma midagi kobisedes ning C# koodi poolt HttpWebRequest.ClientCertificates.Add(cert);

meetod ei ütle kaa midagi paha, et sertifikaati külge panemine on mõttetu, kuna alumised protokollid ei pruugi TLS 1.2 toetada, sertifikaat võetakse küljest Windows Serveri poolt ja veateade mis välja antakse võib olla selline

Server.ClientProxy.SslAuthenticationFailed Client (....) specifies HTTPS but did not supply TLS certificate

Kontrolli FIPS seadistust ja konto õigusi privaatvõtmega sertifikaadile. Kontode puhul tähele panna, et isegi kui ASP.NET rakenduses certStore.Certificates.Find(X509FindType.FindByThumbprint ... leiab privaatvõtmega sertifikaadi ülesse, siis veebipäringu tegemisel ei pruugi see õigus enam toimida vaid vaja on otsest Read õiguse andmist ASP.NET või MS SQL SERVER kontole

TLS 1.2 programmeerimine sõltub tugevalt kasutatavast .NET Framework versioonist, mida kõrgem seda parem. https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls

Telia Xtee turvaserveri poolne veateade Failed to get signing info for member ... Signer.InternalError: Member .... has no suitable certificates tähendab seda, et sa ei saada õiget privaatvõtme sertifikaati, näiteks saadad avalikku võtit kliendi sertifikaadina.

XTee test turvaserveriga kontrolli, et Sertifitseerimiskeskuse test SK serdid oleks laetud test OCSP’sse (https://demo.sk.ee/upload_cert).


Teine võimalus, kuidas privaatvõtmega kliendi sertifikaati kasutades TLS 1.2 SOAP päringut teha on kasutada System.Net.Security.SslStream klassi, võib anda rohkem veateateid välja

    /// <summary>
    /// Telia XTee turvaserveri kaudu kliendi sertifikaadiga TLS 1.2 protokolli abil SOAP päring TcpClient ja SSLStream kaudu
    /// </summary>
    /// <param name="Url">XTee turvaserveri https URL</param>
    /// <param name="envelope">SOAP päringu XML sisu stringina</param>
    /// <param name="serthash">Local Computer / Personal sertifikaadihoidlas oleva sertifikaadi räsi</param>
    /// <param name="serverIp">XTee turvaserveri IP</param>
    /// <param name="servesertname">XTee turvaserveri sertifikaadi nimi</param>
    /// <returns></returns>
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlChars XTeeSOAPSslRequest(SqlString Url, SqlString envelope, SqlString serthash, SqlString serverIp, SqlString servesertname)
    {
        string inputMessage = "";
        string kataloog = (string)tempdir;
        try
        {
            string ServerHostName = (string)serverIp; 
            int ServerPort = 443;
            string ServerCertificateName = (string)servesertname;
            string ymbrik = (string)envelope;
         

            string chash = (string)serthash;

            X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            certStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
            X509Certificate2Collection serts = certStore.Certificates.Find(X509FindType.FindByThumbprint, chash, true);
            if (serts.Count == 0)
            {
                serts = certStore.Certificates.Find(X509FindType.FindByThumbprint, chash, false);
            }
            if (serts.Count != 1)
            {
                throw new SystemException("Sertifikaati räsiga " + chash + "  ei ole hoidlas Local Computer - Personal");
            }
            X509Certificate2 cert = serts[0];
            X509CertificateCollection clientCertificateCollection = new X509CertificateCollection(new X509Certificate[] { serts[0] });
            certStore.Close();
            using (TcpClient client = new TcpClient(ServerHostName, ServerPort))
            {
                using (SslStream sslStream = new SslStream(client.GetStream(), false, App_CertificateValidation))
                {

                    try
                    {
                        sslStream.AuthenticateAsClient(ServerCertificateName, clientCertificateCollection, SslProtocols.Tls12, false);
                    }
                    catch (SystemException ex)
                    {
                        throw new SystemException("SsslStream.AuthenticateAsClient viga(Kontrolli sertifikaadile ligipääsu õigusi) " + ex.Message);
                    }
                    if (sslStream.LocalCertificate == null)
                    {
                        throw new SystemException("SSL LocalCertificate ei ole " + chash);
                    }

                    if (sslStream.RemoteCertificate == null)
                    {
                        throw new SystemException("SSL RemoteCertificate ei ole");
                    }
                    string url = (string)Url;
                    string host = url.Replace("https://", "");
                    host = host.Replace("http://", "");
                    string outputMessage = @"POST " + url + @" HTTP/1.1
Accept: text/xml
Content-Type: text/xml; charset=UTF-8
Content-Length: " + ymbrik.Length + @"
Host: " + host + @"
Connection: Keep-Alive
User-Agent: KAMPUS

" + ymbrik;
                 
                    byte[] outputBuffer = Encoding.UTF8.GetBytes(outputMessage);
                    sslStream.Write(outputBuffer);

                    byte[] inputBuffer = new byte[8096];
                    int inputBytes = 0;
                    while (inputBytes == 0)
                    {
                        inputBytes = sslStream.Read(inputBuffer, 0, inputBuffer.Length);
                    }
                    inputMessage = Encoding.UTF8.GetString(inputBuffer, 0, inputBytes);
                }  //using
            }
        }
        catch (SystemException ex)
        {         
            throw new SystemException(ex.GetType().Name + " " + ex.Message);
        }
        SqlChars sd = new SqlChars(((SqlString)inputMessage));
        return sd;
    }


 private static bool App_CertificateValidation(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
        if (sslPolicyErrors == SslPolicyErrors.None)
        { return true; }
        if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
        { return true; } //siin võid ka false tagastada
     
        return false;
    }

Logi veateade AcquireCredentialsHandle() failed with error 0X8009030D. Exception in HttpWebRequest#62571264:: - The request was aborted: Could not create SSL/TLS secure channel. viitab konto õiguste puudumisele privaatvõtmega sertifikaadile, ilmneb see sslStream.AuthenticateAsClient(ServerCertificateName, clientCertificateCollection, SslProtocols.Tls12, false); meetodi juures.

Rakenduse sujuvamaks töötamiseks on tark teha eraldi testfunktsioon System.Net.Security.SslStream AuthenticateAsClient meetodi väljakutsega, kui see annab tõrke on tegu konto ligipääsu õigustega privaatvõtmega sertifikaadile.