AHKhttp - HTTP Server

Post your working scripts, libraries and tools
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

AHKhttp - HTTP Server

16 Oct 2014, 21:02

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
Last edited by Skittlez on 08 Nov 2014, 18:53, edited 2 times in total.
User avatar
joedf
Posts: 7187
Joined: 29 Sep 2013, 17:08
Facebook: J0EDF
Google: +joedf
GitHub: joedf
Location: Canada
Contact:

Re: AHKhttp - HTTP Server

16 Oct 2014, 23:27

Interesting
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500 @ 4.00 GHz, 2x8GB DDR4 2733 MHz, NVIDIA GTX 1060 6GB | [About Me] | [ASPDM - StdLib Distribution]
[Populate the AHK MiniCity!] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library] | [About the AHK Foundation]
User avatar
fincs
Posts: 500
Joined: 30 Sep 2013, 14:17
GitHub: fincs
Location: Seville, Spain
Contact:

Re: AHKhttp - HTTP Server

17 Oct 2014, 03:24

AutoHotkey on Rails anybody? This little script even supports route-based serving instead of using a folder in disk.
fincs
Windows 10 x64 Build 17134 / AutoHotkey v1.1.29.01
Get SciTE4AutoHotkey v3.0.06.01 - [My project list]
User avatar
joedf
Posts: 7187
Joined: 29 Sep 2013, 17:08
Facebook: J0EDF
Google: +joedf
GitHub: joedf
Location: Canada
Contact:

Re: AHKhttp - HTTP Server

17 Oct 2014, 13:57

Haha true, along with AHK-webkit ;)
Image Image Image Image Image
Windows 10 x64 Professional, Intel i5-8500 @ 4.00 GHz, 2x8GB DDR4 2733 MHz, NVIDIA GTX 1060 6GB | [About Me] | [ASPDM - StdLib Distribution]
[Populate the AHK MiniCity!] | [Qonsole - Quake-like console emulator] | [LibCon - Autohotkey Console Library] | [About the AHK Foundation]
User avatar
Relayer
Posts: 125
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: AHKhttp - HTTP Server

18 Oct 2014, 08:47

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
ahk7
Posts: 198
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

18 Oct 2014, 10:42

@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
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

18 Oct 2014, 11:30

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.
guest3456
Posts: 2555
Joined: 09 Oct 2013, 10:31

Re: AHKhttp - HTTP Server

18 Oct 2014, 14:40

awesome

User avatar
Relayer
Posts: 125
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: AHKhttp - HTTP Server

19 Oct 2014, 08:51

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
ahk7
Posts: 198
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

19 Oct 2014, 12:13

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.
Last edited by ahk7 on 21 Oct 2014, 11:20, edited 1 time in total.
guest3456
Posts: 2555
Joined: 09 Oct 2013, 10:31

Re: AHKhttp - HTTP Server

20 Oct 2014, 17:17

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

lexikos
Posts: 6488
Joined: 30 Sep 2013, 04:07
GitHub: Lexikos

Re: AHKhttp - HTTP Server

20 Oct 2014, 21:49

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
ahk7
Posts: 198
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

21 Oct 2014, 11:23

I can indeed see it is listening using netstat -a so that is a good tip (edited post above)
kidbit
Posts: 168
Joined: 02 Oct 2013, 16:05

Re: AHKhttp - HTTP Server

27 Oct 2014, 13:28

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>
</
question := (2b) || !(2b) © Shakespeare.
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

07 Nov 2014, 22:22

@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.
ahk7
Posts: 198
Joined: 06 Nov 2013, 16:35

Re: AHKhttp - HTTP Server

08 Nov 2014, 09:16

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
Skittlez
Posts: 11
Joined: 13 Mar 2014, 12:16

Re: AHKhttp - HTTP Server

08 Nov 2014, 18:54

Added support for mime types, updated example using ServeFile.
Documentation is on github.
tmplinshi
Posts: 1484
Joined: 01 Oct 2013, 14:57

Re: AHKhttp - HTTP Server

15 Mar 2015, 11:31

Great! Thank you very much!!
tmplinshi
Posts: 1484
Joined: 01 Oct 2013, 14:57

Re: AHKhttp - HTTP Server

17 Mar 2015, 04:44

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!

Return to “Scripts and Functions”

Who is online

Users browsing this forum: No registered users and 41 guests