WebSockets

WebSockets är en av de intressanta nyheterna i HTML5. Tanken med WebSockets är att exponera en säker, klientinitierad tvåvägskanal mellan klient och server. Detta gör att man mycket enkelt kan sno i hop t.ex. ett webbaserat chat-program eftersom chat-texten kan skickas direkt till samtliga deltagare utan att behöva mellanlagras som vid t.ex. pollningsbaserade lösningar.

Protokollet körs med eller utan kryptering (ws:// eller wss://). Den initiala handskakningen för att upprätta en WebSocket är kompatibel med HTTP. Därför kan man dela port 80 och 443 med en webbserver om denna har stöd för WebSockets. Protokollet är minimalt rambaserat och stödjer både binär och UTF-8 data.

Bakgrund

Om du är redan är bekant med alternativen till WebSockets kan du hoppa över detta avsnitt. För er andra följer en kort bakgrund till hur webben fungerat innan HTML5.

De tidiga versionerna av HTML var framför allt till för att presentera sidor med statisk information. De förändrades inte över tid. Med introduktionen av CGI kunde man producera sidor som förändrades vid omladdning, men full dynamik fick man först med DHTML och JavaScript. Exempelvis kan din webbmejl uppdateras när nya mejl kommer in utan att sidan laddas om.

Webbklienten kan genom s.k. kort pollning regelbundet kolla med servern efter nya mejl eller chat-meddelanden genom asynkrona HTTP anrop, t.ex. GET http://example.com/user/123/mail. Här behöver man justera in pollningsintervallet, d.v.s. hur ofta ska vi kolla efter nya mejl. För ofta resulterar i många anrop till servern när brevlådan är tom. Detta är resurskrävande, speciellt på serversidan. En stor del av kapaciteten går åt till att sätta upp TCP och skicka HTTP headrar. Vittjas brevlådan för sällan blir upplevelsen inte så dynamisk.

Ett annat alternativ är s.k. lång pollning där anropet till servern bara besvaras ommedelbart om det finns några brev. Om brevlådan är tom håller servern HTTP-anropet öppet tills ett brev har kommit in. Först då skickas resultat. Klient ligger väntar på resultat i bakgrunden och uppdaterar sitt gränssnitt så fort det kommer. Därefter gör den direkt ett nytt anrop.

Nackdelen med denna variant är att servern (eller servrarna) måste ha en hög kapacitet eftersom att samtliga inloggade användare har en öppen TCP-koppling. Applikationsprotokollet är dessutom HTTP vilket innebär en ganska kraftig merkostnad eftersom HTTP:s anrops- och svars-huvuden utgör en stor del av den totala informationen. Man kan dessutom få problem om klienten direkt skapar en ny uppkoppling mot servern vid alla typer av svar, t.ex. Internal Server error. Detta kan få servern att falla över.

Använder man inte JavaScript utan Java Applets eller Flash kan man tänka sig andra protokoll än HTTP eftersom man då har tillgång till råa TCP-socketar. Tyvärr stöter man ofta på problem med mellanliggande brandväggar, routrar och andra proxytjänster som avsiktligt eller oavsiktligt modifierar eller blockerar trafik.

Eftersom de flesta företags brandväggar är inställda på att släppa igenom HTTP trafik (port 80 och 443) har man valt att definiera WebSockets så att den kan samköras med HTTP. På så sätt maximeras WebSockets tillgänglighet och användbarhet.

WebSocket-protokollet

Klienten initierar kopplingen genom att skicka följande text:

GET /demo HTTP/1.1
 
Upgrade: WebSocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
Sec-WebSocket-Key2: 12998 5 Y3 1  .P00

^n:ds[4U

Servern kan då godkänna uppgraderingen till WebSocket genom följande svar:

HTTP/1.1 101 WebSocket Protocol Handshake

Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample

8jKS'y:G*Co,Wxa-

WebSocket innehåller en ganska komplicerad handskakningshash, för att säkerställa att man via WebSocket API:et inte kan göra en protokoll-injicering och för att se att mottagande server verkligen har förstått att det är en uppgradering av protokollet till WebSockets som pågår inte är ett vanligt HTTP-anrop.

En protokollinjicering innebär att information i handskakningen som API:et fått från klienten (t.ex. URL och metadata) är konstruerad på sådant sätt att den förändrar innebörden i handskakningen (jämför med SQL-injicering). Man vill alltså förhindra att handskakningen resulterar i att en bakdörr öppnas.

Efter en lyckad handskakning skickas data i ramar (frames) i bägge riktningarna. Textramar börjar med 0x00 och slutar med 0xFF med UTF-8 text emellan. Binära ramar kommer stödjas i framtiden.

Webbläsarstöd och säkerhet

Stödet för WebSockets hos de stora webbläsarna är för tillfället oklart. Nyligen stängde Firefox 4 av WebSocketstödet som standard efter det att en artikel publicerats där man påstår att det finns säkerhetsluckor i protokollet. Transparent Proxies: Threat or Menace?.

Jag har läst artikeln grundligt och kan jag säga att problemet man tittat på först och främst inte har så mycket att göra med WebSockets utan mer med de säkerhetshål som kan uppstå om det finns s.k. transparenta proxytjänter mellan klient och målserver. Detta är inte ovanligt på företag som leder all trafik via en proxy för att t.ex. övervaka, berika eller cacha innehållet.

På vilket sätt berör nu detta WebSockets? Jo, eftersom WebSockets kommer att samköras med HTTP över port 80, finns det risk att en angripare skickar HTTP-anrop (genom t.ex. chatfönstret) insprängda i dataramarna. En proxy som inte är medveten om Websockets kan då felaktigt tolka datat som om det vore HTTP. Enda sättet runt detta är att använda sig av maskning.

Exampelkod

Om man vill experimentera själv så går det utmärkt att köra WebSockets i de flesta moderna browsers.

Jettys demoapp, ett chat-exempel, som distribueras med Jetty 7 innehöll några kompileringsfel. Antagligen för att Jettys API är under utveckling. Du kan komma åt en fungerande version på https://github.com/cygni-stacktrace/websockets.

Som synes kommer det initiala handskaknings GET:et till doGet() och skickas direkt vidare till Jettys default-servlet. Denna har stöd för WebSockets och anropar doWebSocketConnect() som skapar en initial WebSocket.

public class WebSocketChatServlet extends WebSocketServlet {
    private final Set<ChatWebSocket> _members = new CopyOnWriteArraySet<ChatWebSocket>();

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {

        getServletContext().getNamedDispatcher("default").
                forward(request, response);
    }

    protected WebSocket doWebSocketConnect(HttpServletRequest request,
                                           String protocol) {
        return new ChatWebSocket();
    }

    class ChatWebSocket implements WebSocket {
        Outbound _outbound;

        public void onConnect(Outbound outbound) {
            _outbound = outbound;
            _members.add(this);
        }

        public void onMessage(byte frame, byte[] data, int offset, int length) {

        }

        public void onMessage(byte frame, String data) {
            for (ChatWebSocket member : _members) {
                try {
                    member._outbound.sendMessage(frame, data);
                } catch (IOException e) {
                    Log.warn(e);
                }
            }
        }

        public void onDisconnect() {
            _members.remove(this);
        }

        public void onFragment(boolean more, byte opcode, byte[] data,
                               int offset, int length) {
        }
    }
}

På klient-sidan så körs följande Javascript-app.. Lägg den i src/main/webapp/ tillsammans med följande web.xml.

src/main/webapp/web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app
   xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
   version="2.5">

  <display-name>Test WebApp</display-name>
  <servlet>
    <servlet-name>WSChat</servlet-name>
    <servlet-class>com.acme.WebSocketChatServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>WSChat</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

Maven pom.xml: /project/dependencies

<dependency>
    <groupId>org.eclipse.jetty</groupId> 
    <artifactId>jetty-websocket</artifactId> 
    <version>7.2.2.v20101205</version> 
    <type>jar</type> 
    <scope>compile</scope> 
</dependency

Maven pom.xml: /project/build/plugins

<plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>7.2.2.v20101205</version>
    <configuration>
        <scanIntervalSeconds>10</scanIntervalSeconds>
    </configuration>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-websocket</artifactId>
            <version>7.2.2.v20101205</version>
        </dependency>
    </dependencies>
</plugin>

Den skarpögde noterar att Jetty har flyttat till eclipse.org. Läs mer om det på Jettys FAQ.