Page 1 of 4

AHKhttp - HTTP Server

Posted: 16 Oct 2014, 21:02
by Skittlez
Basic http server I wrote, requires AHKsock.

Example

Code: Select all

#Persistent
#SingleInstance, force
SetBatchLines, -1

paths := {}
paths["/"] := Func("HelloWorld")
paths["404"] := Func("NotFound")
paths["/logo"] := Func("Logo")

server := new HttpServer()
server.LoadMimes(A_ScriptDir . "/mime.types")
server.SetPaths(paths)
server.Serve(8000)
return

Logo(ByRef req, ByRef res, ByRef server) {
    server.ServeFile(res, A_ScriptDir . "/logo.png")
    res.status := 200
}

NotFound(ByRef req, ByRef res) {
    res.SetBodyText("Page not found")
}

HelloWorld(ByRef req, ByRef res) {
    res.SetBodyText("Hello World")
    res.status := 200
}

#include, %A_ScriptDir%\AHKhttp.ahk
#include <AHKsock>
Source

Code: Select all

class Uri
{
    Decode(str) {
        Loop
            If RegExMatch(str, "i)(?<=%)[\da-f]{1,2}", hex)
                StringReplace, str, str, `%%hex%, % Chr("0x" . hex), All
            Else Break
        Return, str
    }

    Encode(str) {
        f = %A_FormatInteger%
        SetFormat, Integer, Hex
        If RegExMatch(str, "^\w+:/{0,2}", pr)
            StringTrimLeft, str, str, StrLen(pr)
        StringReplace, str, str, `%, `%25, All
        Loop
            If RegExMatch(str, "i)[^\w\.~%]", char)
                StringReplace, str, str, %char%, % "%" . Asc(char), All
            Else Break
        SetFormat, Integer, %f%
        Return, pr . str
    }
}

class HttpServer
{
    static servers := {}

    LoadMimes(file) {
        if (!FileExist(file))
            return false

        FileRead, data, % file
        types := StrSplit(data, "`n")
        this.mimes := {}
        for i, data in types {
            info := StrSplit(data, " ")
            type := info.Remove(1)
            ; Seperates type of content and file types
            info := StrSplit(LTrim(SubStr(data, StrLen(type) + 1)), " ")

            for i, ext in info {
                this.mimes[ext] := type
            }
        }
        return true
    }

    GetMimeType(file) {
        default := "text/plain"
        if (!this.mimes)
            return default

        SplitPath, file,,, ext
        type := this.mimes[ext]
        if (!type)
            return default
        return type
    }

    ServeFile(ByRef response, file) {
        f := FileOpen(file, "r")
        length := f.RawRead(data, f.Length)
        f.Close()

        response.SetBody(data, length)
        res.headers["Content-Type"] := this.GetMimeType(file)
    }

    SetPaths(paths) {
        this.paths := paths
    }

    Handle(ByRef request) {
        response := new HttpResponse()
        if (!this.paths[request.path]) {
            func := this.paths["404"]
            response.status := 404
            if (func)
                func.(request, response, this)
            return response
        } else {
            this.paths[request.path].(request, response, this)
        }
        return response
    }

    Serve(port) {
        this.port := port
        HttpServer.servers[port] := this

        AHKsock_Listen(port, "HttpHandler")
    }
}

HttpHandler(sEvent, iSocket = 0, sName = 0, sAddr = 0, sPort = 0, ByRef bData = 0, bDataLength = 0) {
    static sockets := {}

    if (!sockets[iSocket]) {
        sockets[iSocket] := new Socket(iSocket)
        AHKsock_SockOpt(iSocket, "SO_KEEPALIVE", true)
    }
    socket := sockets[iSocket]

    if (sEvent == "DISCONNECTED") {
        socket.request := false
        sockets[iSocket] := false
    } else if (sEvent == "SEND") {
        if (socket.TrySend()) {
            socket.Close()
        }

    } else if (sEvent == "RECEIVED") {
        server := HttpServer.servers[sPort]

        text := StrGet(&bData, "UTF-8")
        request := new HttpRequest(text)

        ; Multipart request
        if (request.IsMultipart()) {
            length := request.headers["Content-Length"]
            request.bytesLeft := length + 0

            if (request.body) {
                request.bytesLeft -= StrLen(request.body)
            }
            socket.request := request
        } else if (socket.request) {
            ; Get data and append it to the request body
            socket.request.bytesLeft -= StrLen(text)
            socket.request.body := socket.request.body . text
        }

        if (socket.request) {
            request := socket.request
            if (request.bytesLeft <= 0) {
                request.done := true
            }
        }

        response := server.Handle(request)
        if (response.status) {
            socket.SetData(response.Generate())

            if (socket.TrySend()) {
                if (!request.IsMultipart() || (request.IsMultipart() && request.done)) {
                    socket.Close()
                }
            }
        }
    }
}

class HttpRequest
{
    __New(data = "") {
        if (data)
            this.Parse(data)
    }

    GetPathInfo(top) {
        results := []
        while (pos := InStr(top, " ")) {
            results.Insert(SubStr(top, 1, pos - 1))
            top := SubStr(top, pos + 1)
        }
        this.method := results[1]
        this.path := Uri.Decode(results[2])
        this.protocol := top
    }

    GetQuery() {
        pos := InStr(this.path, "?")
        query := StrSplit(SubStr(this.path, pos + 1), "&")
        if (pos)
            this.path := SubStr(this.path, 1, pos - 1)

        this.queries := {}
        for i, value in query {
            pos := InStr(value, "=")
            key := SubStr(value, 1, pos - 1)
            val := SubStr(value, pos + 1)
            this.queries[key] := val
        }
    }

    Parse(data) {
        this.raw := data
        data := StrSplit(data, "`n`r")
        headers := StrSplit(data[1], "`n")
        this.body := LTrim(data[2], "`n")

        this.GetPathInfo(headers.Remove(1))
        this.GetQuery()
        this.headers := {}

        for i, line in headers {
            pos := InStr(line, ":")
            key := SubStr(line, 1, pos - 1)
            val := Trim(SubStr(line, pos + 1), "`n`r ")

            this.headers[key] := val
        }
    }

    IsMultipart() {
        length := this.headers["Content-Length"]
        expect := this.headers["Expect"]

        if (expect = "100-continue" && length > 0)
            return true
        return false
    }
}

class HttpResponse
{
    __New() {
        this.headers := {}
        this.status := 0
        this.protocol := "HTTP/1.1"

        this.SetBodyText("")
    }

    Generate() {
        FormatTime, date,, ddd, d MMM yyyy HH:mm:ss
        this.headers["Date"] := date

        headers := this.protocol . " " . this.status . "`n"
        for key, value in this.headers {
            headers := headers . key . ": " . value . "`n"
        }
        headers := headers . "`n"
        length := this.headers["Content-Length"]

        buffer := new Buffer((StrLen(headers) * 2) + length)
        buffer.WriteStr(headers)

        buffer.Append(this.body)
        buffer.Done()

        return buffer
    }

    SetBody(ByRef body, length) {
        this.body := new Buffer(length)
        this.body.Write(&body, length)
        this.headers["Content-Length"] := length
    }

    SetBodyText(text) {
        this.body := Buffer.FromString(text)
        this.headers["Content-Length"] := this.body.length
    }


}

class Socket
{
    __New(socket) {
        this.socket := socket
    }

    Close(timeout = 5000) {
        AHKsock_Close(this.socket, timeout)
    }

    SetData(data) {
        this.data := data
    }

    TrySend() {
        if (!this.data || this.data == "")
            return false

        p := this.data.GetPointer()
        length := this.data.length

        this.dataSent := 0
        loop {
            if ((i := AHKsock_Send(this.socket, p, length - this.dataSent)) < 0) {
                if (i == -2) {
                    return
                } else {
                    ; Failed to send
                    return
                }
            }

            if (i < length - this.dataSent) {
                this.dataSent += i
            } else {
                break
            }
        }
        this.dataSent := 0
        this.data := ""

        return true
    }
}

class Buffer
{
    __New(len) {
        this.SetCapacity("buffer", len)
        this.length := 0
    }

    FromString(str, encoding = "UTF-8") {
        length := Buffer.GetStrSize(str, encoding)
        buffer := new Buffer(length)
        buffer.WriteStr(str)
        return buffer
    }

    GetStrSize(str, encoding = "UTF-8") {
        encodingSize := ((encoding="utf-16" || encoding="cp1200") ? 2 : 1)
        ; length of string, minus null char
        return StrPut(str, encoding) * encodingSize - encodingSize
    }

    WriteStr(str, encoding = "UTF-8") {
        length := this.GetStrSize(str, encoding)
        VarSetCapacity(text, length)
        StrPut(str, &text, encoding)

        this.Write(&text, length)
        return length
    }

    ; data is a pointer to the data
    Write(data, length) {
        p := this.GetPointer()
        DllCall("RtlMoveMemory", "uint", p + this.length, "uint", data, "uint", length)
        this.length += length
    }

    Append(ByRef buffer) {
        destP := this.GetPointer()
        sourceP := buffer.GetPointer()

        DllCall("RtlMoveMemory", "uint", destP + this.length, "uint", sourceP, "uint", buffer.length)
        this.length += buffer.length
    }

    GetPointer() {
        return this.GetAddress("buffer")
    }

    Done() {
        this.SetCapacity("buffer", this.length)
    }
}
GitHub
Documentation

Re: AHKhttp - HTTP Server

Posted: 16 Oct 2014, 23:27
by joedf
Interesting

Re: AHKhttp - HTTP Server

Posted: 17 Oct 2014, 03:24
by fincs
AutoHotkey on Rails anybody? This little script even supports route-based serving instead of using a folder in disk.

Re: AHKhttp - HTTP Server

Posted: 17 Oct 2014, 13:57
by joedf
Haha true, along with AHK-webkit ;)

Re: AHKhttp - HTTP Server

Posted: 18 Oct 2014, 08:47
by Relayer
This looks very interesting but I would like to ask someone to explain a little more how one would use this. It would be very helpful.

Relayer

Re: AHKhttp - HTTP Server

Posted: 18 Oct 2014, 10:42
by ahk7
@Skittlez: do you plan to work on this some more and extend it?

Some documentation would indeed be useful, I've been playing around with a bit - see below.

You can find AHKsock here https://github.com/jleb/AHKsock

Here is a small example displaying parameters and using them in a form. Enter two numbers and press submit will show you the answer on the next page.

Code: Select all

#Persistent
#SingleInstance, force
SetBatchLines, -1

ahp=
(
<html>
<title>AHKhttp-server 1.0</title>
<body>
<b>[var]</b>
</body>
</html>
)

paths := {}
paths["/"] := Func("PlayList")
paths["/page"] := Func("Page")
paths["/calc"] := Func("Calc")
paths["404"] := Func("NotFound")

server := new HttpServer()
server.SetPaths(paths)
server.Serve(8000)
Run http://localhost:8000/page?para1=123&para2=456
return

NotFound(ByRef req, ByRef res) {
    res.SetBody("Page not found")
}

Page(ByRef req, ByRef res) {
	global ahp
	form=
	(
<form action="/calc" method="get">
<input type=text name=para1> 
+ 
<input type=text name=para2>
<input type=submit>
</form>
	)
	stringreplace, serve, ahp, [var], % "para1: " req.queries["para1"] " - para2: " req.queries["para2"] "<br><br>" form, All
    res.SetBody(serve)
    res.statusCode := 200
}

Calc(ByRef req, ByRef res) {
	global ahp
	answer:=req.queries["para1"] + req.queries["para2"]
	stringreplace, serve, ahp, [var], % req.queries["para1"] "+" req.queries["para2"] "=" answer, All
    res.SetBody(serve)
    res.statusCode := 200
}

HelloWorld(ByRef req, ByRef res) {
    res.SetBody("Hello World")
    res.statusCode := 200
}

f12::Reload
	
#include, %A_ScriptDir%\AHKhttp.ahk
#include AHKsock.ahk

Re: AHKhttp - HTTP Server

Posted: 18 Oct 2014, 11:30
by Skittlez
I'll write some documentation later today.
@ahk7 If you have any features you'd like to see, just let me know and I'll see what I can do.

Re: AHKhttp - HTTP Server

Posted: 18 Oct 2014, 14:40
by guest3456
awesome

Re: AHKhttp - HTTP Server

Posted: 19 Oct 2014, 08:51
by Relayer
ahk7,

I tried your script and get "unable to connect". I'm using Firefox and AutoHotKey v1.1.16.05

I'm a complete noob when it comes to http server stuff.

Relayer

Re: AHKhttp - HTTP Server

Posted: 19 Oct 2014, 12:13
by ahk7
if you google 'unable to connect localhost firefox' you will see that it might be a firefox setting so try it with another browser to see if that works - if it does you can try to figure out how to let firefox allow connections to localhost.
You can test if the server is up and running by opening a command window (cmd.exe) and use ping localhost if it gives valid pings the script (server) works - so you will have to work on getting it to work in your browser. (Edit see comments by Lexikos below)

I'd like to be able to display images in pages and stream mp3 over http - I recall sparrow did that by reading/parsing a list of mimetypes.

Edit: ping but note the test script does a run Run http://localhost:8000/page?para1=123&para2=456 so the default browser should try to open http://localhost:8000 - it may be a firefox problem so worth trying in another browser just to check. Otherwise I don't know why it doesn't work or how to fix it.

Re: AHKhttp - HTTP Server

Posted: 20 Oct 2014, 17:17
by guest3456
Relayer wrote:ahk7,

I tried your script and get "unable to connect". I'm using Firefox and AutoHotKey v1.1.16.05

I'm a complete noob when it comes to http server stuff.

Relayer
Well why don't you tell us what url you are trying to connect to in firefox

The example in the OP uses port 8000 instead of the http default which is 80

Re: AHKhttp - HTTP Server

Posted: 20 Oct 2014, 21:49
by lexikos
You can test if the server is up and running by opening a command window (cmd.exe) and use ping localhost if it gives valid pings the script (server) works
That will only prove that the computer responds to ICMP (specifically, ping) requests on its loopback interface. It will not prove that the HTTP server is running or that the system will accept TCP/IP connections on any particular port. Some systems don't respond to ping (from outside) but still accept connections on specific TCP ports. I think you'd be hard-pressed to find a system that doesn't respond to ping localhost.

What you can do is attempt to connect to localhost:8000 using telnet or PuTTY, or see if port 8000 is listed by netstat -a.

Of course, that won't be necessary if the problem is as guest3456 guessed.
http://localhost:8000
not http://localhost

Re: AHKhttp - HTTP Server

Posted: 21 Oct 2014, 11:23
by ahk7
I can indeed see it is listening using netstat -a so that is a good tip (edited post above)

Re: AHKhttp - HTTP Server

Posted: 27 Oct 2014, 13:28
by kidbit
There's some bug: last 5 symbols of the page get eaten.
ahk7's example results into a page with such a code:

Code: Select all

<html>
<title>AHKhttp-server 1.0</title>
<body>
<b>para1: 123 - para2: 456<br><br><form action="/calc" method="get">
<input type=text name=para1>
+
<input type=text name=para2>
<input type=submit>
</form></b>
</body>
</

Re: AHKhttp - HTTP Server

Posted: 07 Nov 2014, 22:22
by Skittlez
@ahk7 I've added an example of how to serve images.
@kidbit Try updating to the latest version, I can't reproduce that bug.

I'll be adding support for mime types and writing some docs, for real this time, sometime this weekend.

Re: AHKhttp - HTTP Server

Posted: 08 Nov 2014, 09:16
by ahk7
Thanks!

I've already been playing around with it and now have a rudimentary mp3 server. I can access the server from other PCs in the network and "stream" MP3 via a downloadable m3u playlist, very nice.

For others who already want to play around with it, you can use the following mimetypes for mp3/m3u

for m3u: (playlists)
res.headers["Content-Type"] := "audio/x-mpequrl"

for mp3:
res.headers["Content-Type"] := "audio/mpeg"

This will play an mp3 in your browser:

Code: Select all

mp3(ByRef req, ByRef res) {
    f := FileOpen(A_ScriptDir . "/groove.mp3", "r") ; example mp3 file
    length := f.RawRead(data, f.Length)
    f.Close()
    res.status := 200
    res.headers["Content-Type"] := "audio/mpeg"
    res.SetBody(data, length)
}
Look forward to the documentation

Re: AHKhttp - HTTP Server

Posted: 08 Nov 2014, 09:55
by geek
Binary files over HTTP. Wonderful! Now we need to dump WAVs from the Speech API over http

Re: AHKhttp - HTTP Server

Posted: 08 Nov 2014, 18:54
by Skittlez
Added support for mime types, updated example using ServeFile.
Documentation is on github.

Re: AHKhttp - HTTP Server

Posted: 15 Mar 2015, 11:31
by tmplinshi
Great! Thank you very much!!

Re: AHKhttp - HTTP Server

Posted: 17 Mar 2015, 04:44
by tmplinshi
For anyone who want to use this wonderful lib:
  1. Please use AHK Unicode version in order to work.
  2. ahk7's example code not working? Yes, it's outdated (I tried his code several times, and finally noticed that today...).
    You need make these changes in his code:
    • Replace SetBody to SetBodyText
    • Replace statusCode to status
Thanks for your example code ahk7!