"R.Wieser" <address@is.invalid>: May 25 09:39AM +0200
Hello all, I've been trying whip up some code using (an older version of) OpenSSL. I've ofcourse googled for information how to use it, but all I've been able to find is some "examples" which stop directly after reading the reply (no code to terminate the connection), or overly complex stuff aimed at a certain target. Currently I have got some code which shows : 1) a BIO_xxx way to retrieve unencrypted data 2) a mix between winsock and SSL_xxx to retrieve encrypted data - not sure how/if the SSL part is closed 3) a mix between BIO_xxx and SSL_xxx to retrieve encrypted data, but which doesn't include code to close the connection. 4) some other bits-and-pieces The thing is, although I can mostly find the used commands back, I can't find anything about how they interact. As such I have a hard time to even find how to close an SSL connecion before I close the underlying BIO/socket one. And for closing the BIO one I'm /assuming/ that all I need is "BIO_free_all" ... Also, 3) does stuff with BIO_xxx which caters to the SSL connection (like setting the name of the target domain), while with 2) I only have to provide the connected socket to SSL_set_fd to have everything work. IOW, I would like to see some BIO_xxx code to connect (which it only seems to do on the first BIO_read or BIO_write), and than have the SSL_xxx part take over the connection the same way as with a standard socket. One other thing : To keep my first steps as easy as possible I'm using blocking sockets. Examples that jump into the deep end using async sockets are of little value & use to me. tl;dr: I'm looking for some tutorial-style example code, explaining the basic setup, request, retrieval and tear-down steps of an encrypted connection using OpenSSL. I'm currently using OpenSSL 0.9.8.0 , but that might just be because I've not come across anything related to the current v3.1.0 version ... Regards, Rudy Wieser |
Paavo Helde <eesnimi@osa.pri.ee>: May 25 03:26PM +0300
25.05.2023 10:39 R.Wieser kirjutas: > using OpenSSL. > I'm currently using OpenSSL 0.9.8.0 , but that might just be because I've > not come across anything related to the current v3.1.0 version ... OpenSSL 0.9 is seriously out of date. Currently supported stable versions are 1.1.* and 3.0.*. I do not have polished example code, my actual code is scattered around several C++ classes and in partially platform-dependent source files. Anyway, I can paste some snippets here, maybe these are helpful. This code is using OpenSSL 3.0.8 for wrapping an existing connected client-side cocked into SSL (Linux and Windows): // Set short socket read timeout for SSL_connect, to avoid indefinite hanging ::setsockopt(socketHandle, SOL_SOCKET, SO_RCVTIMEO, // ... platform-dependent code ... // Create new SSL connection state object if ((ssl_ = SSL_new(context_->GetSSL_CTX())) == nullptr) { // handle error } // Set up SNI (Server Name Indication), some websites like Google complain if this is not done. SSL_set_tlsext_host_name(ssl_, host.c_str()); // Attach the SSL session to the socket descriptor SSL_set_fd(ssl_, static_cast<int>(socketHandle)); // When using the SSL_connect(3) or SSL_accept(3) routines, calling of SSL_set_connect_state() or SSL_set_accept_state() is not needed. int ret = SSL_connect(ssl_); if (ret == 1) { // OK, connected } // set the socket timeout larger if needed ... ********* write ****** int n = SSL_write(ssl_, buf, static_cast<int>(len)); if (n != static_cast<int>(len)) { // handle error ******* read ***** ERR_clear_error(); int n = SSL_read(ssl_, buf, static_cast<int>(len)); if (n < 0) { // handle errpr int k = SSL_get_error(ssl_, n); if (k == SSL_ERROR_WANT_READ) { // timeout, or maybe the server side closed connection? // the caller should reconnect and retry } else { // handle other error } } ********* cleanup ******** if (socketHandle != INVALID_SOCKET) { closesocket(socketHandle); } if (ssl_) { SSL_free(ssl_); } HTH |
Paavo Helde <eesnimi@osa.pri.ee>: May 25 03:52PM +0300
25.05.2023 15:26 Paavo Helde kirjutas: >> (no >> code to terminate the connection), or overly complex stuff aimed at a >> certain target. BTW, ChatGPT4 seemed to be pretty strong with openssl, so you might just ask it to generate some example programs. It even changed the code to use proper std::unique_ptr lifetime management when I suggested that. |
Muttley@dastardlyhq.com: May 25 03:15PM
On Thu, 25 May 2023 15:26:17 +0300 >25.05.2023 10:39 R.Wieser kirjutas: >This code is using OpenSSL 3.0.8 for wrapping an existing connected >client-side cocked into SSL (Linux and Windows): I wouldn't use Windows socket code in Linux as windows sockets are severely hobbled by not being able to be multiplexed on select() due to Microsofts incomprehensible decision to make sockets a special handle type instead of just a file descriptor meaning you can't mix them with other open file types or inputs and they have to be converted into win messages or some such overcomplicated crap in order to be received asynchronously unless you decide to make life your even harder and spawn a new thread for each socket connection. |
"R.Wieser" <address@is.invalid>: May 25 05:38PM +0200
Paavo, > OpenSSL 0.9 is seriously out of date. Currently supported stable versions > are 1.1.* and 3.0.*. Yeah, I know. I already downloaded v3.1.0. But up until I can find example and/or tutorial code about/for it its rather useless to me. And as I could find info about that "seriously out of date" version that became the one I'm working with. > This code is using OpenSSL 3.0.8 for wrapping an existing connected > client-side cocked into SSL (Linux and Windows): That is quite similar to the 2) code I mentioned (using a pre-created and connected native socket). I would like to keep everything in OpenSSL. I would like to see a solution where I can create and connect a BIO socket in a similar way I create a native socket (not providing hostname information) and than use the BIO socket in the code you have posted. The other solution I'm looking for is a full SSL_xxx one. No BIO_xxx or native socket. By the way: you use "context_->GetSSL_CTX()", but I do not see you cleanup the returned handle. It /might/ be done by something else, and that is exacly what I meant where I say that I can find reference to commands, but not how they interact. For "SSL_free" I found this : https://www.openssl.org/docs/man1.0.2/man3/SSL_free.html Under "remarks" it says [quote] SSL_free() also calls the free()ing procedures for indirectly affected items, if applicable: the buffering BIO, the read and write BIOs, cipher lists specially created for this ssl, the SSL_SESSION. [/quote] If it refers to the above "GetSSL_CTX" handle they have it pretty-well hidden. :-( Last remark : a quick search on four search-engines for that "GetSSL_CTX" function turns up exactly nothing ... Regards, Rudy Wieser |
"R.Wieser" <address@is.invalid>: May 25 05:51PM +0200
Muttley, > I wouldn't use Windows socket code in Linux :-) I would not want to use it the other way around either. But thats a quite big "what if" step you're making there - what makes you think that I would want to do that ? I think I asked how I could do the whole thing just using (BIO_xxx and) SSL_xxx functions. Besides the "I need example code of the whole thing, from setting up upto cleaning up" ofcourse. On the other hand, for my blocking handles implementation I probably need a "select" somewhere, so my code doesn't stall waiting forever for a reply/more data while the other side is doing the same. Regards, Rudy Wieser |
Muttley@dastardlyhq.com: May 25 04:01PM
On Thu, 25 May 2023 17:51:06 +0200 >:-) I would not want to use it the other way around either. >But thats a quite big "what if" step you're making there - what makes you >think that I would want to do that ? I don't care one way or the other, I was replying to Paavo, not you. |
scott@slp53.sl.home (Scott Lurndal): May 25 04:13PM
>and/or tutorial code about/for it its rather useless to me. And as I could >find info about that "seriously out of date" version that became the one I'm >working with. Have you a copy of the O'reilly openssl book? There's a copy on github. |
Paavo Helde <eesnimi@osa.pri.ee>: May 25 07:43PM +0300
> or inputs and they have to be converted into win messages or some such > overcomplicated crap in order to be received asynchronously unless you decide > to make life your even harder and spawn a new thread for each socket connection. I agree life would be easier if I would only need to support UNIX everything-is-a-file concepts. Alas, that's not the case for me. BTW, the replacement of select() in Windows is WaitForMultipleObjects(). Windows messages are not needed (which is good because usually I have no window which could receive them). Anyway, sockets low-level programming is not a recommended approach. Suggesting libcurl or Boost ASIO instead, which take care of some aspects. OTOH, if things go south (read: corporate networks with prescribed security regulations) it might be harder to get multiple layers of software working, instead of a single layer. |
Paavo Helde <eesnimi@osa.pri.ee>: May 25 08:12PM +0300
25.05.2023 18:38 R.Wieser kirjutas: > information) and than use the BIO socket in the code you have posted. > The other solution I'm looking for is a full SSL_xxx one. No BIO_xxx or > native socket. For client sockets I do not use any BIO* functions. > By the way: you use "context_->GetSSL_CTX()", but I do not see you cleanup > the returned handle. Right. The GetSSL_CTX() is my own function which returns a pointer to shared SSL_CTX structure. This SSL_CTX is created once, is shared by all SSL client connections and will be released only at the end of the program. Setting up this context involves loading the trusted CA certificates from various locations, and setting up a verify callback which logs errors or warning, but also verifies the certificate against the Windows system cert store, on Windows. I can list the functions I use for setting up and releasing this SSL_CTX, in call order (error checks omitted for brevity): SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); const SSL_METHOD* method = TLS_client_method(); SSL_CTX* context = SSL_CTX_new(method); SSL_CTX_set_default_verify_paths(context); // These can be called 0 or more times SSL_CTX_load_verify_file(context, trustedCaBundleFile.c_str()); SSL_CTX_load_verify_dir(context, trustedCaCertDir.c_str()) SSL_CTX_set_verify(context, SSL_VERIFY_PEER, MySSLVerifyCallback); ***** cleanup ********* SSL_CTX_free(context); ******** callback ************* NOTE: contains my own functions, you need to adapt creatively int MySSLVerifyCallback(int preverify_ok, X509_STORE_CTX *x509_ctx) { try { if (preverify_ok) { // openssl has verified the cert, OK. return 1; // success } X509* err_cert = (x509_ctx? X509_STORE_CTX_get_current_cert(x509_ctx): nullptr); const char* errMessage; int err; if (x509_ctx) { err = X509_STORE_CTX_get_error(x509_ctx); errMessage = X509_verify_cert_error_string(err); } else { err = -1; errMessage = "x509_ctx==nullptr"; } // Verify the cert against the global Windows certificates store (ACINF-3817). if (VerifyTLS(err_cert)) { return 1; // success } char buf[256] = { 0 }; if (err_cert) { X509_NAME_oneline(X509_get_subject_name(err_cert), buf, sizeof(buf) - 1); } std::string msg = Sprintf("SSL cert verify error: num=%d: %s: %s")(err)(errMessage)(buf); // handle as an error or as a warning return 1; // success return 0; // failure } catch (...) { // handle error return 0; // failure } } ********** VerifyTLS() for Windows ************** NOTE: contains my own functions, you need to adapt creatively // Need a separate function because OPENSSL_free is a macro. void FreeOpenSslBuffer(unsigned char* buffer) { OPENSSL_free(buffer); } bool VerifyTLS(X509* cert) { // openssl failed to verify the cert. // Try to verify the cert against Windows cert store unsigned char* buffer = nullptr; int len = i2d_X509(cert, &buffer); if (len < 0) { throw Exception(ERR_IO, "i2d_X509() failed"); } ASSERT(buffer); DEBUG_ASSERT(len > 0); ON_BLOCK_EXIT(FreeOpenSslBuffer, buffer); CERT_CHAIN_PARA thing; ::memset(&thing, 0, sizeof(thing)); thing.cbSize = sizeof(thing); PCCERT_CONTEXT winContext = ::CertCreateCertificateContext(X509_ASN_ENCODING, buffer, len); if (!winContext) { syserr_t errorCode = GetLastErrorCode(); throw Exception(ERR_IO, "CertCreateCertificateContext() failed: " + GetSysErrorString(errorCode)); } ON_BLOCK_EXIT(::CertFreeCertificateContext, winContext); PCCERT_CHAIN_CONTEXT chain = nullptr; if (::CertGetCertificateChain( nullptr, winContext, nullptr, nullptr, &thing, CERT_CHAIN_CACHE_END_CERT|CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY, nullptr, &chain)) { ON_BLOCK_EXIT(::CertFreeCertificateChain, chain); DWORD errorStatus = chain->TrustStatus.dwErrorStatus; if (errorStatus == 0) { return true; // Windows approved the cert } } return false; } |
"R.Wieser" <address@is.invalid>: May 25 09:18PM +0200
Paavo, > For client sockets I do not use any BIO* functions. That still means you could be using either native or SSL_xxx sockets. >> By the way: you use "context_->GetSSL_CTX()", but I do not see you >> cleanup the returned handle. > Right. The GetSSL_CTX() is my own function That explains me not finding it, but also means that your offered code is of the same level as most of what I can find on the web : incomplete, leaving me guess to what else to get it to work is needed. :-\ > Setting up this context involves loading the trusted CA certificates from > various locations, Done, > and setting up a verify callback which logs errors or warning, The example I found uses "SSL_get_verify_result" . Which makes me remember that I still need to find a demo domain which purposely has a wrong cerificate - to test if the code I have actually works. > but also verifies the certificate against the Windows system cert store, > on Windows. PEM Currently I'm using SSL_CTX_load_verify_locations, using a local file. I might, in the future, try to find out how I can access Windows own cert store for it, but as said I want to keep the current code as simple/basic as possible. > I can list the functions I use for setting up and releasing this SSL_CTX, > in call order (error checks omitted for brevity): I see a *lot* more code than I'm currently using myself. I can likely write something with /around that, but it would just be a "monkey see, monkey do" result - no real understanding of what I'm (effectivily) copying. :-( By the way, the below is what I started with. First a basic non-ssl retrieval of a webpage, afterwards the same retrieval but now over SSL. http://jokinkuang.github.io/2015/02/27/how_to_do_http_&_https_request_with_openssl.html As you can see it does most everything with BIO_xxx calls, with just a sliver of SSL_xxx related calls thrown in. This one https://gist.githubusercontent.com/endSly/8369715/raw/ee86f381e814499bdffd34267b5e371b21839ecb/sslconnect.c uses a native socket, connects it and than gives it to the SSL_xxx layer. There is no "BIO_set_conn_hostname" (or an SSL_xxx version of it!) in sight anywhere, but it seems to work nonwithstanding (probably extracting the domain name and port from the native socket) I've got a number of other snippets, but as I said, most are incomplete or (far) beyond a basic setup. Like this one (which has thrown a number of snippets on a single page): https://cpp.hotexamples.com/examples/-/-/SSL_connect/cpp-ssl_connect-function-examples.html Regards, Rudy Wieser |
Keith Thompson <Keith.S.Thompson+u@gmail.com>: May 25 02:05PM -0700
>>working with. > Have you a copy of the O'reilly openssl book? > There's a copy on github. Do you mean "Network Security with OpenSSL" by John Viega, Matt Messier, and Pravir Chandra? The last update was June 2002, when the latest release was 0.9.7d. I found a PDF copy of the book on GitHub. It appears to be pirated. O'Reilly sometimes releases some books for free, but they haven't done so with this one. -- Keith Thompson (The_Other_Keith) Keith.S.Thompson+u@gmail.com Will write code for food. void Void(void) { Void(); } /* The recursive call of the void */ |
Pavel <pauldontspamtolk@removeyourself.dontspam.yahoo>: May 24 09:47PM -0400
> With knowledge ande skill, thine endeavor shalodour. > May this Middle English spelinge, inspirynge thyne queste, > Illuminate C++'s misteries ande bringe thee the beste. WOW. Just WOW. The back-handed compliment in the last verse is my favorite. Thanks! |
Muttley@dastardlyhq.com: May 25 09:04AM
On Wed, 24 May 2023 21:47:19 -0400 >> Illuminate C++'s misteries ande bringe thee the beste. >WOW. Just WOW. The back-handed compliment in the last verse is my >favorite. Thanks! Personally I find its abilities rather concerning since they're only going to improve exponentially. |
Michael S <already5chosen@yahoo.com>: May 25 06:48AM -0700
> >favorite. Thanks! > Personally I find its abilities rather concerning since they're only > going to improve exponentially. Does it really resembles Middle English or just an arbitrary funny misspelling? |
David Brown <david.brown@hesbynett.no>: May 25 04:17PM +0200
>> favorite. Thanks! > Personally I find its abilities rather concerning since they're only > going to improve exponentially. It will improve in some ways, and not in others. It can always get faster and more flexible (though not "exponentially" so). But current AI bots suffer from several critical flaws, and it is not easy to see how they can be fixed. They base their knowledge on what they find online, but have no way to judge the veracity of the sources, they have no concept of error margins and reliability of the information they have processed or produce, no understanding of ethics or appropriateness (beyond a few simple pattern-matching rules rushed in to cut down on the worst abuses). This all means it has no idea when it is telling the truth, and when it is lying or simply making things up. When making this poem, it has no idea if the sources it used for the language were real Middle English, modernised spelling of Middle English, made up pretend Middle English, sources from 1066, sources from 1500, or whatever. It doesn't really matter for a fun sample like this - it looks very impressive, though it would make even the most amateur Chaucer fan cringe. But until there are major fundamental changes, the use of such AI tools is severely limited because they can't be trusted to have any basis in truth or reality. Still, they can cheat in exams, write speeches for politicians and work as journalists for the Daily Mail and Fox news, and no one will notice. |
Muttley@dastardlyhq.com: May 25 03:24PM
On Thu, 25 May 2023 06:48:34 -0700 (PDT) >> going to improve exponentially. >Does it really resembles Middle English or just an arbitrary funny misspell= >ing? Better than I could do and I like to think it couldn't just conjure up these sorts of spellings but did learn them somewhere. |
Muttley@dastardlyhq.com: May 25 03:27PM
On Thu, 25 May 2023 16:17:30 +0200 >AI bots suffer from several critical flaws, and it is not easy to see >how they can be fixed. They base their knowledge on what they find >online, but have no way to judge the veracity of the sources, they have Neither do humans frankly. >processed or produce, no understanding of ethics or appropriateness >(beyond a few simple pattern-matching rules rushed in to cut down on the >worst abuses). Neither do some humans. It doesn't mean they can't think. You're missing the elephant in the room - this thing might not be concious but it is thinking , its not a dumb stats based system like a markov model or similar. >Still, they can cheat in exams, write speeches for politicians and work >as journalists for the Daily Mail and Fox news, and no one will notice. Only after they've worked their way up from writing for woke comics such as The Guardian. |
David Brown <david.brown@hesbynett.no>: May 25 08:49PM +0200
>> how they can be fixed. They base their knowledge on what they find >> online, but have no way to judge the veracity of the sources, they have > Neither do humans frankly. True - but at least some of us can do a lot better job. No one is immune to being fooled, and some people get fooled a /lot/, but these AI systems have not yet reached the level of even the daftest flat-earthers and "Q anon" followers. > Neither do some humans. It doesn't mean they can't think. You're missing > the elephant in the room - this thing might not be concious but it is > thinking , its not a dumb stats based system like a markov model or similar. I understand that, and I'm not missing anything. The key, I think, is that humans go through many years of learning and upbringing - these systems are like a child that has read thousands of books, but has never been taught about trustworthy sources, never seen anything bad happen, never made mistakes and been corrected. They are highly knowledgeable and totally naïve. They need patient teachers and "parents", working with them for many years at least, and no one is willing to wait for that. |
David Brown <david.brown@hesbynett.no>: May 25 08:58PM +0200
On 25/05/2023 15:48, Michael S wrote: >> Personally I find its abilities rather concerning since they're only >> going to improve exponentially. > Does it really resembles Middle English or just an arbitrary funny misspelling? It vaguely resembles Middle English - at least some of the funny spellings are reasonable versions (transliterated into modern letters). But it appears to have been done by making up a poem about C++ in modern English (no mean feat in itself), then translating word for word where the system could find a Middle English equivalent (with no regard for context or consistency of time and place - Middle English spanned 400 years and was significantly different in different parts of the British Isles). Some modern words have been modified by patterns common at the time in an attempt at following Middle English grammar, others have not. As a test of how authentic it is, I guess that most people in this group can understand most of the poem. But if you look up some Chaucer or other real Middle English poetry, I doubt you'll have much idea at all about what it is saying - at least not without a lot of effort. |
Pavel <pauldontspamtolk@removeyourself.dontspam.yahoo>: May 24 09:13PM -0400
Öö Tiib wrote: > how many destructors to call with delete[] to pointer allocated with > new[] is not exposed to programmers for to write such a class > themselves if the standard library does not bother. Actually, I meant someone slightly different. The shame is that the vector class is not factored onto an exact-size array (let's call it "block") and the rest of vector functionality. vector has the design flaw that it is too many things: it is the owner of several contiguous objects in memory but also the growth strategy. The block is useful independently of vector if: a) a custom growth strategy is needed; or b) no growth is needed at all (a very often case in my practice). For small vectors, replacing them with blocks could provide significant space savings (not limited by only capacity even when compiler trickery is not used; e.g. block<size_t> could be specialized so that the underlying storage layout is implemented like this: struct { const size_t N; size_t data[]; }; // or like this struct { const size_t *end; size_t data[]; }; This way, 2 words of memory are saved (capacity and begin pointer) and one indirection is also saved when one needs to access elements in a block passed by reference or pointer so this brings both performance and space savings. Then, as I said, for the purpose of implementing the block (again, maybe of only some specializations), using compiler trickery for computing N *could* be justified. *could* is a keyword here. It *could* also be useless or even detrimental performance-wise. It is feasible that the operator new[] would actually take more memory from the OS for N elements than (non-array) allocation function void* operator new(size[, align_val_t]...) would for the same N elements. In this case, no gain is achieved from using operator new[] and compiler trickery over using the single-object form of allocation function "operator new" and system-independent implementation. -Pavel |
Pavel <pauldontspamtolk@removeyourself.dontspam.yahoo>: May 24 09:16PM -0400
>> shared_ptr. I think the C++ designers thought this isn't necessary. > Well they thought it necessary to be able to store arrays and index them > inside a shared pointer, so why didn't they complete the job? The support of indexing can be considered to be the support of the existing functionality of a raw pointer as it also supports indexing, but not iteration. -Pavel |
Pavel <pauldontspamtolk@removeyourself.dontspam.yahoo>: May 24 09:40PM -0400
Alf P. Steinbach wrote: >> shared_ptr instatiation *type* > It is. > For example, otherwise my example of how to do this would not work. Yes, but notice that for Mutley, it seemed more natural that N was the the run-time size of the array (somewhere up this thread he used the argument that the compiler is able to get N to destroy every of N allocated objects when delete[] is called -- on raw pointer -- that does not have compile-time N). This tells me that different people would feel differently about which N is "natural" (compile-time or runtime) so standardizing this would be counter-intuitive either way. >> shared_ptr<mystruct[3]> sp(new mystruct[getNOfMyStructs()]); >> What can possibly go wrong? (TM) > You shot your leg off. I certainly did. This is exactly why I do not mind if this particular way to self-inflict harm on myself is not supported :-). smart pointers were supposed to be safer than the raw ones, after all, and even raw pointers do not support iteration. > C++ allows that, in general. Yes, that was one of the design goals. Blow up your whole leg.. Going on tangent, I am still mad at them they broke this performance-over-safety promise when they decided that most-derived-class's virtual function cannot be called from a constructor or destructor of a base class. "What's safe enough for Java should be safe enough for C++" says I :-). > shared_ptr provides support for arrays, I disagree. I think what you mean by "support for array" (indexing?) is actually the support for the raw pointers to memory allocated by operator new[]. The argument that we pass to the shared_ptr constructor in shared_ptr<mystruct[3]> sp(new mystruct[3]); (and that the shared_ptr likely stores underneath) is such a raw pointer, not an array. > including I believe restrictions > on conversions, but not total type safety enforcement. > - Alf -Pavel |
"Chris M. Thomasson" <chris.m.thomasson.1@gmail.com>: May 24 09:20PM -0700
On 5/24/2023 6:40 PM, Pavel wrote: >>>>>> shared pointers? >>>>> These shared pointers do not store arrays. They store pointers to >>>>> memory object allocated with operator new[]. [...] > shared_ptr<mystruct[3]> sp(new mystruct[3]); > (and that the shared_ptr likely stores underneath) is such a raw > pointer, not an array. [...] Almost has to be. |
Muttley@dastardlyhq.com: May 25 08:51AM
On Wed, 24 May 2023 09:48:05 -0700 (PDT) >Target: x86_64-pc-linux-gnu >Thread model: posix >InstalledDir: /usr/bin Beats me. I'm using clang on a Mac and I tried it again in case I'd not cut and pasted properly but it still failed. However this wouldn't be the first time the Apple version of Clang differs from versions on other platforms. |