diff --git a/apng.d b/apng.d index 433f4c7..5a3d8f9 100644 --- a/apng.d +++ b/apng.d @@ -57,6 +57,7 @@ class ApngFrame { auto height = frameControlChunk.height; auto bytesPerLine = bytesPerLineOfPng(parent.header.depth, parent.header.type, width); + bytesPerLine--; // removing filter byte from this calculation since we handle separtely int idataIdx; ubyte[] idata; @@ -79,11 +80,7 @@ class ApngFrame { this.data = idata; } - // then need to uncompress it - // and unfilter it... - // and then convert it to the right format. - - MemoryImage frameData; + //MemoryImage frameData; } class ApngAnimation { @@ -109,16 +106,16 @@ enum APNG_BLEND_OP : byte { OVER = 1 } -void readApng(in ubyte[] data) { +ApngAnimation readApng(in ubyte[] data) { auto png = readPng(data); auto header = PngHeader.fromChunk(png.chunks[0]); - Color[] palette; - if(header.type == 3) { - palette = fetchPalette(png); - } auto obj = new ApngAnimation(); + if(header.type == 3) { + obj.palette = fetchPalette(png); + } + bool seenIdat = false; bool seenFctl = false; @@ -227,11 +224,13 @@ void readApng(in ubyte[] data) { expectedSequenceNumber++; // and the rest of it is a datastream... - obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload; + obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $]; break; default: // ignore } } + + return obj; } diff --git a/argon2.d b/argon2.d new file mode 100644 index 0000000..be51c43 --- /dev/null +++ b/argon2.d @@ -0,0 +1,91 @@ +/++ + My minimal interface to https://github.com/p-h-c/phc-winner-argon2 + + You must compile and install the C library separately. ++/ +module arsd.argon2; + +// it is conceivably useful to hash the password with a secret key before passing to this function, +// but I'm not going to do that automatically here just to keep this thin and simple. + +import core.stdc.stdint; + +pragma(lib, "argon2"); + +extern(C) +int argon2id_hash_encoded( + const uint32_t t_cost, + const uint32_t m_cost, + const uint32_t parallelism, + const void *pwd, const size_t pwdlen, + const void *salt, const size_t saltlen, + const size_t hashlen, char *encoded, + const size_t encodedlen); + +extern(C) +int argon2id_verify(const char *encoded, const void *pwd, + const size_t pwdlen); + +enum ARGON2_OK = 0; + +/// Parameters to the argon2 function. Bigger numbers make it harder to +/// crack, but also take more resources for legitimate users too +/// (e.g. making logins and signups slower and more memory-intensive). Some +/// examples are provided. HighSecurity is about 3/4 second on my computer, +/// MediumSecurity about 1/3 second, LowSecurity about 1/10 second. +struct SecurityParameters { + uint cpuCost; + uint memoryCost; /// in KiB fyi + uint parallelism; +} + +/// ditto +enum HighSecurity = SecurityParameters(8, 512_000, 8); +/// ditto +enum MediumSecurity = SecurityParameters(4, 256_000, 4); +/// ditto +enum LowSecurity = SecurityParameters(2, 128_000, 4); + +/// Check's a user's provided password against the saved password, and returns true if they matched. Neither string can be empty. +bool verify(string savedPassword, string providedPassword) { + return argon2id_verify((savedPassword[$-1] == 0 ? savedPassword : (savedPassword ~ '\0')).ptr, providedPassword.ptr, providedPassword.length) == ARGON2_OK; +} + +/// encode a password for secure storage. verify later with [verify] +string encode(string password, SecurityParameters params = MediumSecurity) { + char[256] buffer; + enum HASHLEN = 80; + + import core.stdc.string; + + ubyte[32] salt = void; + + version(linux) {{ + import core.sys.posix.unistd; + import core.sys.posix.fcntl; + int fd = open("/dev/urandom", O_RDONLY); + auto ret = read(fd, salt.ptr, salt.length); + assert(ret == salt.length); + close(fd); + }} else { + import std.random; + foreach(ref s; salt) + s = cast(ubyte) uniform(0, 256); + } + + auto ret = argon2id_hash_encoded( + params.cpuCost, + params.memoryCost, + params.parallelism, + password.ptr, password.length, + salt.ptr, salt.length, + HASHLEN, // desired size of hash. I think this is fine being arbitrary + buffer.ptr, + buffer.length + ); + + if(ret != ARGON2_OK) + throw new Exception("wtf"); + + return buffer[0 .. strlen(buffer.ptr) + 1].idup; +} diff --git a/cgi.d b/cgi.d index 23c605b..dcf9992 100644 --- a/cgi.d +++ b/cgi.d @@ -1109,7 +1109,7 @@ class Cgi { /// - void writeToFile(string filenameToSaveTo) { + void writeToFile(string filenameToSaveTo) const { import std.file; if(contentInMemory) std.file.write(filenameToSaveTo, content); @@ -5003,6 +5003,143 @@ private struct SerializationBuffer { will have to have dump and restore too, so i can restart without losing stuff. */ + +/++ + A convenience object for talking to the [BasicDataServer] from a higher level. + See: [getSessionObject] + + You pass it a `Data` struct describing the data you want saved in the session. + Then, this class will generate getter and setter properties that allow access + to that data. + + Note that each load and store will be done as-accessed; it doesn't front-load + mutable data nor does it batch updates out of fear of read-modify-write race + conditions. (In fact, right now it does this for everything, but in the future, + I might batch load `immutable` members of the Data struct.) + + At some point in the future, I might also let it do different backends, like + a client-side cookie store too, but idk. ++/ +class Session(Data) { + private Cgi cgi; + private string sessionId; + + /// You probably don't want to call this directly, see [getSessionObject] instead. + this(Cgi cgi) { + this.cgi = cgi; + if(auto ptr = "sessionId" in cgi.cookies) + sessionId = (*ptr).length ? *ptr : null; + } + + /++ + Starts a new session. Note that a session is also + implicitly started as soon as you write data to it, + so if you need to alter these parameters from their + defaults, be sure to explicitly call this BEFORE doing + any writes to session data. + + Params: + idleLifetime = How long, in seconds, the session + should remain in memory when not being read from + or written to. The default is one day. + + NOT IMPLEMENTED + + useExtendedLifetimeCookie = The session ID is always + stored in a HTTP cookie, and by default, that cookie + is discarded when the user closes their browser. + + But if you set this to true, it will use a non-perishable + cookie for the given idleLifetime. + + NOT IMPLEMENTED + +/ + void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { + assert(sessionId is null); + + // FIXME: what if there is a session ID cookie, but no corresponding session on the server? + + import std.random, std.conv; + sessionId = to!string(uniform(1, long.max)); + + BasicDataServer.connection.createSession(sessionId, idleLifetime); + setCookie(); + } + + private void setCookie() { + cgi.setCookie( + "sessionId", sessionId, + 0 /* expiration */, + null /* path */, + null /* domain */, + true /* http only */, + cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); + } + + /++ + Regenerates the session ID and updates the associated + cookie. + + This is also your chance to change immutable data + (not yet implemented). + +/ + void regenerateId() { + if(sessionId is null) { + start(); + return; + } + import std.random, std.conv; + auto oldSessionId = sessionId; + sessionId = to!string(uniform(1, long.max)); + BasicDataServer.connection.renameSession(oldSessionId, sessionId); + setCookie(); + } + + /++ + Terminates this session, deleting all saved data. + +/ + void terminate() { + BasicDataServer.connection.destroySession(sessionId); + sessionId = null; + setCookie(); + } + + /++ + Plain-old-data members of your `Data` struct are wrapped here via + the opDispatch property getters and setters. + + If the member is a non-string array, it returns a magical array proxy + object which allows for atomic appends and replaces via overloaded operators. + You can slice this to get a range representing a $(B const) view of the array. + This is to protect you against read-modify-write race conditions. + +/ + @property typeof(__traits(getMember, Data, name)) opDispatch(string name)() { + if(sessionId is null) + return typeof(return).init; + + auto v = BasicDataServer.connection.getSessionData(sessionId, name); + if(v.length == 0) + return typeof(return).init; + import std.conv; + return to!(typeof(return))(v); + } + + /// ditto + @property void opDispatch(string name)(typeof(__traits(getMember, Data, name)) value) { + if(sessionId is null) + start(); + import std.conv; + BasicDataServer.connection.setSessionData(sessionId, name, to!string(value)); + } +} + +/++ + Gets a session object associated with the `cgi` request. ++/ +Session!Data getSessionObject(Data)(Cgi cgi) { + return new Session!Data(cgi); +} + /++ +/ @@ -5047,10 +5184,20 @@ final class BasicDataServerImplementation : BasicDataServer, EventIoServer { sessions.remove(oldSessionId); } void setSessionData(string sessionId, string dataKey, string dataValue) { + if(sessionId !in sessions) + createSession(sessionId, 3600); // FIXME? sessions[sessionId].values[dataKey.idup] = dataValue.idup; } string getSessionData(string sessionId, string dataKey) { - return sessions[sessionId].values[dataKey]; + if(auto session = sessionId in sessions) { + if(auto data = dataKey in (*session).values) + return *data; + else + return null; // no such data + + } else { + return null; // no session + } } @@ -5757,10 +5904,17 @@ ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { switch to choose if you want to override. */ -struct DispatcherDefinition(alias dispatchHandler) {// if(is(typeof(dispatchHandler("str", Cgi.init) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; +struct DispatcherDefinition(alias dispatchHandler) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; alias handler = dispatchHandler; string urlPrefix; bool rejectFurther; + DispatcherDetails details; +} + +// tbh I am really unhappy with this part +struct DispatcherDetails { + string filename; + string contentType; } private string urlify(string name) { @@ -6428,7 +6582,7 @@ auto serveApi(T)(string urlPrefix) { import arsd.dom; import arsd.jsvar; - static bool handler(string urlPrefix, Cgi cgi) { + static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { auto obj = new T(); obj.initialize(cgi); @@ -6780,7 +6934,7 @@ class CollectionOf(Obj, Helper = void) : RestObject!(Helper) { +/ auto serveRestObject(T)(string urlPrefix) { assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); - static bool handler(string urlPrefix, Cgi cgi) { + static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { string url = cgi.pathInfo[urlPrefix.length .. $]; if(url.length && url[$ - 1] == '/') { @@ -7071,15 +7225,26 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp if(filename is null) filename = urlPrefix[1 .. $]; if(contentType is null) { - + if(filename.endsWith(".png")) + contentType = "image/png"; + if(filename.endsWith(".jpg")) + contentType = "image/jpeg"; + if(filename.endsWith(".html")) + contentType = "text/html"; + if(filename.endsWith(".css")) + contentType = "text/css"; + if(filename.endsWith(".js")) + contentType = "application/javascript"; } - static bool handler(string urlPrefix, Cgi cgi) { - //cgi.setResponseContentType(contentType); - //cgi.write(std.file.read(filename), true); - cgi.write(std.file.read(urlPrefix[1 .. $]), true); + + static bool handler(string urlPrefix, Cgi cgi, DispatcherDetails details) { + if(details.contentType.indexOf("image/") == 0) + cgi.setCache(true); + cgi.setResponseContentType(details.contentType); + cgi.write(std.file.read(details.filename), true); return true; } - return DispatcherDefinition!handler(urlPrefix, true); + return DispatcherDefinition!handler(urlPrefix, true, DispatcherDetails(filename, contentType)); } auto serveRedirect(string urlPrefix, string redirectTo) { @@ -7108,15 +7273,15 @@ auto serveStaticData(string urlPrefix, const(void)[] data, string contentType) { +/ bool dispatcher(definitions...)(Cgi cgi) { // I can prolly make this more efficient later but meh. - foreach(definition; definitions) { + static foreach(definition; definitions) { if(definition.rejectFurther) { if(cgi.pathInfo == definition.urlPrefix) { - auto ret = definition.handler(definition.urlPrefix, cgi); + auto ret = definition.handler(definition.urlPrefix, cgi, definition.details); if(ret) return true; } } else if(cgi.pathInfo.startsWith(definition.urlPrefix)) { - auto ret = definition.handler(definition.urlPrefix, cgi); + auto ret = definition.handler(definition.urlPrefix, cgi, definition.details); if(ret) return true; } diff --git a/dom.d b/dom.d index 1f1cbe0..fbad39c 100644 --- a/dom.d +++ b/dom.d @@ -819,7 +819,7 @@ class Document : FileResource { } if(strict) - enforce(data[pos] == '>');//, format("got %s when expecting >\nContext:\n%s", data[pos], data[pos - 100 .. pos + 100])); + enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[pos - 100 .. pos + 100])); else { // if we got here, it's probably because a slash was in an // unquoted attribute - don't trust the selfClosed value diff --git a/jsvar.d b/jsvar.d index 696a490..5f9e591 100644 --- a/jsvar.d +++ b/jsvar.d @@ -654,6 +654,10 @@ struct var { static if(is(typeof(__traits(getMember, t, member)) == function)) { // skipping these because the delegate we get isn't going to work anyway; the object may be dead and certainly won't be updated //this[member] = &__traits(getMember, proxyObject, member); + + //but for simple toString, I'll allow it. or maybe not it doesn't work right. + //static if(member == "toString" && is(typeof(&__traits(getMember, t, member)) == string delegate())) + //this[member] = &__traits(getMember, t, member); } else this[member] = __traits(getMember, t, member); } @@ -1241,6 +1245,12 @@ struct var { return v; } + @property static var emptyObject(var prototype) { + if(prototype._type == Type.Object) + return var.emptyObject(prototype._payload._object); + return var.emptyObject(); + } + @property PrototypeObject prototypeObject() { var v = prototype(); if(v._type == Type.Object) diff --git a/png.d b/png.d index 8b618d1..bdebb07 100644 --- a/png.d +++ b/png.d @@ -231,7 +231,7 @@ void convertPngData(ubyte type, ubyte depth, const(ubyte)[] data, int width, uby break; default: assert(0); } - assert(data.length == 0, "not all consumed, wtf ");// ~ to!string(h)); + assert(data.length == 0, "not all consumed, wtf " ~ to!string(data)); } /* @@ -1482,6 +1482,7 @@ struct LazyPngFile(LazyPngChunksProvider) } // FIXME: doesn't handle interlacing... I think +// note it returns the length including the filter byte!! @nogc @safe pure nothrow int bytesPerLineOfPng(ubyte depth, ubyte type, uint width) { immutable bitsPerChannel = depth; diff --git a/postgres.d b/postgres.d index e69dc6e..2ffb423 100644 --- a/postgres.d +++ b/postgres.d @@ -81,6 +81,8 @@ class PostgreSql : Database { auto res = PQexec(conn, toStringz(sql)); int ress = PQresultStatus(res); + // https://www.postgresql.org/docs/current/libpq-exec.html + // FIXME: PQresultErrorField can get a lot more info in a more structured way if(ress != PGRES_TUPLES_OK && ress != PGRES_COMMAND_OK) throw new DatabaseException(error()); diff --git a/simpleaudio.d b/simpleaudio.d index 91b7ac2..8b0b21d 100644 --- a/simpleaudio.d +++ b/simpleaudio.d @@ -198,6 +198,17 @@ import core.thread; final class AudioPcmOutThread : Thread { /// this() { + version(linux) { + // this thread has no business intercepting signals from the main thread, + // so gonna block a couple of them + import core.sys.posix.signal; + sigset_t sigset; + auto err = sigfillset(&sigset); + assert(!err); + err = sigprocmask(SIG_BLOCK, &sigset, null); + assert(!err); + } + super(&run); } diff --git a/simpledisplay.d b/simpledisplay.d index e799ab4..c2aeed3 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -848,8 +848,6 @@ version(Windows) { // http://wiki.dlang.org/Simpledisplay.d -// FIXME: SIGINT handler is necessary to clean up shared memory handles upon ctrl+c - // see : http://www.sbin.org/doc/Xlib/chapt_09.html section on Keyboard Preferences re: scroll lock led // Cool stuff: I want right alt and scroll lock to do different stuff for personal use. maybe even right ctrl @@ -1351,6 +1349,14 @@ class SimpleWindow : CapableOfHandlingNativeEvent, CapableOfBeingDrawnUpon { is the drawable space inside; it excludes the title bar, etc.) Windows based on images will not be resizable and do not use OpenGL. + + It will draw the image in upon creation, but this will be overwritten + upon any draws, including the initial window visible event. + + You probably do not want to use this and it may be removed from + the library eventually, or I might change it to be a "permanent" + background image; one that is automatically drawn on it before any + other drawing event. idk. +/ this(Image image, string title = null) { this(image.width, image.height, title); @@ -2345,6 +2351,7 @@ private: version(X11) { __gshared int customEventFD = -1; + __gshared int customSignalFD = -1; } else version(Windows) { __gshared HANDLE customEventH = null; } @@ -2713,6 +2720,12 @@ struct EventLoop { impl.notExited = false; } + version(linux) + ref void delegate(int) signalHandler() { + assert(impl !is null); + return impl.signalHandler; + } + static EventLoopImpl* impl; } @@ -2741,6 +2754,8 @@ struct EventLoopImpl { static import unix = core.sys.posix.unistd; static import err = core.stdc.errno; import core.sys.linux.timerfd; + + void delegate(int) signalHandler; } version(X11) { @@ -2843,6 +2858,29 @@ struct EventLoopImpl { throw new Exception("can't create eventfd for custom event processing"); } } + + if (customSignalFD == -1) { + import core.sys.linux.sys.signalfd; + + sigset_t sigset; + auto err = sigemptyset(&sigset); + assert(!err); + err = sigaddset(&sigset, SIGINT); + assert(!err); + err = sigaddset(&sigset, SIGHUP); + assert(!err); + err = sigprocmask(SIG_BLOCK, &sigset, null); + assert(!err); + + customSignalFD = signalfd(-1, &sigset, SFD_NONBLOCK); + assert(customSignalFD != -1); + + ep.epoll_event ev = void; + { import core.stdc.string : memset; memset(&ev, 0, ev.sizeof); } // this makes valgrind happy + ev.events = ep.EPOLLIN; + ev.data.fd = customSignalFD; + ep.epoll_ctl(epollFd, ep.EPOLL_CTL_ADD, customSignalFD, &ev); + } } SimpleWindow.processAllCustomEvents(); // process events added before event FD creation @@ -2909,8 +2947,10 @@ struct EventLoopImpl { dispose(); } - version(X11) + version(linux) ref int customEventFD() { return SimpleWindow.customEventFD; } + version(linux) + ref int customSignalFD() { return SimpleWindow.customSignalFD; } version(Windows) ref auto customEventH() { return SimpleWindow.customEventH; } @@ -2957,7 +2997,22 @@ struct EventLoopImpl { assert(fd != -1); // should never happen cuz the api doesn't do that but better to assert than assume. auto flags = events[idx].events; if(flags & ep.EPOLLIN) { - if(fd == display.fd) { + if (fd == customSignalFD) { + version(linux) { + import core.sys.linux.sys.signalfd; + import core.sys.posix.unistd : read; + signalfd_siginfo info; + read(customSignalFD, &info, info.sizeof); + + auto sig = info.ssi_signo; + + if(EventLoop.get.signalHandler !is null) { + EventLoop.get.signalHandler()(sig); + } else { + EventLoop.get.exit(); + } + } + } else if(fd == display.fd) { version(sdddd) { import std.stdio; writeln("X EVENT PENDING!"); } this.mtLock(); scope(exit) this.mtUnlock(); @@ -4628,8 +4683,8 @@ version(X11) { setX11Selection!"SECONDARY"(window, text); } - /// - void setX11Selection(string atomName)(SimpleWindow window, string text) { + /// The `after` delegate is called after a client requests the UTF8_STRING thing. it is a mega-hack right now! + void setX11Selection(string atomName)(SimpleWindow window, string text, void delegate() after = null) { assert(window !is null); auto display = XDisplayConnection.get(); @@ -4673,6 +4728,9 @@ version(X11) { event.target, 8 /* bits */, 0 /* PropModeReplace */, text.ptr, cast(int) text.length); + + if(after) + after(); } else { selectionEvent.property = None; // I don't know how to handle this type... }