0
\$\begingroup\$

I am trying to detect the content/mime type of a given Buffer/string. In other words, I am trying to guess what on earth is a given string.

My approach is somewhat brute-force:

/**
 * Check if valid url encoded.
 * For now I set the value as true
 * @param {String} body 
 * @returns {Boolean}
 */
const isValidFormUrlEncoded = (body) => {
    return /^(?:(?:\w+)(?:\[(?:\d*|'[^']*')\])*=[\w\-\+\.%]*(?:&|$))+$/.test(body);
}

/**
 * Detects mime type and file extention from request body
 * @param {String} body request Body 
 * @param {Function} callback (err,mime,extention,dataBuffer)  
 */
const detectBodyMime = (body,callback) => {
    let buffer = isBase64(body)?Buffer.from(body,'base64'):Buffer.from(body);
  
    const magic = new mmm.Magic(mmm.MAGIC_MIME_TYPE);

    magic.detect(buffer, function(err, result) {
        
        if (err) {  return callback(err); }

        if(result == 'text/plain') {
            let text = buffer.toString();
            if (text.codePointAt(0) === 0xFEFF) { // UTF8 BOM
                text = text.substring(1);
            }
            
            try {   
                JSON.parse(text);
                return callback(null,'application/json','json',buffer);
            }catch(e){                
                // Do nothing keep it silent we need to just verify that content is Json
            }

            if(isValidFormUrlEncoded(text)){
                return callback(null,'application/x-www-form-urlencoded',null,buffer);
            }


            if(text.indexOf('Content-Disposition: form-data; name="')>1){
                return callback(null,'multipart/form-data',null,buffer);
            }

            try {
                yaml.load(text);
                return callback(null,'text/yaml','yml',buffer);
            } catch (e) {
                //
            }
        }
        
        return callback(null,result,mime.extension(result),buffer);
    });
}

I made a dummy usage stript:

const {detectBodyMime} = require('../src/common/http_utils');

const yaml = `
# it is used for development and contributions
# not intented to use for production
version: "3"

services:

  manipulator:
    build:
      context: .
      dockerfile: DockerfileDev
    networks:
      frontend:
        ipv4_address: 172.21.0.2
    volumes:
      - .:/home/node/app
      - ./config/config.json:/etc/http_manipulator/config.json
      - ./config/cert.conf:/etc/http_manipulator/cert.conf
      - ./config_vol:/etc/http_manipulator

  # manipulator_prod:
  #   build:
  #     context: .
  #     dockerfile: Dockerfile
  #   networks:
  #     frontend:
  #       ipv4_address: 172.21.0.3
  #   volumes:
  #     - ./config_vol_prod:/etc/http_manipulator

networks:
  frontend:
    ipam:
      config:
        - subnet: 172.21.0.0/24
`;

detectBodyMime(yaml,(err,mime,extention,buffer) => {
    console.log("3",mime,extention);
});

const json=`
{
    "name":"hello",
    "val":"json"
}
`

detectBodyMime(json,(err,mime,extention,buffer) => {
    console.log("2",mime,extention);
});

const multipart=`

--------------------------ca72190f498016c6
Content-Disposition: form-data; name="key1"

value1
--------------------------ca72190f498016c6
Content-Disposition: form-data; name="key2"

value2
--------------------------ca72190f498016c6
Content-Disposition: form-data; name="myfile"; filename="exclude.txt"
Content-Type: text/plain

pleas=help&me=plz&var[]=true&var[2]=false&var['blahblah']=ipsum

--------------------------ca72190f498016c6--
`

detectBodyMime(multipart,(err,mime,extention,buffer) => {
    console.log("1",mime,extention);
});

const urlencodedForm=`pleas=help&me=plz&var[]=true&var[2]=false&var['blahblah']=ipsum`;
detectBodyMime(urlencodedForm,(err,mime,extention)=>{
    console.log("5",mime,extention);
});

const urlencodedForms=`pleas=Content-Disposition%3A%20form-data%3B%20name%3D%22myfile%22%3B%20filename%3D%22exclude.txt%22&me=plz&var[]=true&var[2]=false&var['blahblah']=ipsum`;

detectBodyMime(urlencodedForms,(err,mime,extention)=>{
    console.log("6",mime,extention);
});

And tested as:

const fs = require('node:fs');
const path = require('path');

const {detectBodyMime} = require('../../../src/common/http_utils.js');

const fileDataPath = path.resolve(__dirname,'__testdata__');

test("detectsBase64EncodedImage",(done)=>{
    const imgdata = path.resolve(fileDataPath,"img.jpeg.base64");
    const content = fs.readFileSync(imgdata);
    
    detectBodyMime(content.toString(),(err,mime,extention)=>{
        expect(mime).toBe('image/jpeg');
        expect(extention).toBe('jpeg');
        expect(err).toBe(null);

        done();
    });
});

test("detectsBase64EncodedImagePng",(done)=>{
    const imgdata = path.resolve(fileDataPath,"img.png.base64");
    const content = fs.readFileSync(imgdata);
    
    detectBodyMime(content.toString(),(err,mime,extention)=>{
        expect(mime).toBe('image/png');
        expect(extention).toBe('png');
        expect(err).toBe(null);

        done();
    });
});

test("detectsBase64EncodedJson",(done)=>{
    const content =  Buffer.from("{\"param\":1}", 'utf8');
    
    detectBodyMime(content.toString('base64'),(err,mime,extention)=>{
        expect(mime).toBe('application/json');
        expect(extention).toBe('json');
        expect(err).toBe(null);

        done();
    });
});


test("detectsJson",(done)=>{
    const content = "{\"param\":1}";
    
    detectBodyMime(content,(err,mime,extention)=>{
        expect(mime).toBe('application/json');
        expect(extention).toBe('json');
        expect(err).toBe(null);

        done();
    });
});

test("detectsXml",(done)=>{
    const content = `<?xml version="1.0" encoding="UTF-8"?>
    <note>
      <to>Tove</to>
      <from>Jani</from>
      <heading>Reminder</heading>
      <body>Don't forget me this weekend!</body>
    </note>
    `;

    detectBodyMime(content,(err,mime,extention)=>{
        expect(['application/xml','text/xml']).toContain(mime);
        expect(extention).toBe('xml');
        expect(err).toBe(null);

        done();
    });
})

test("detectsXmlBase64 encoded",(done)=>{
    const content = `<?xml version="1.0" encoding="UTF-8"?>
    <note>
      <to>Tove</to>
      <from>Jani</from>
      <heading>Reminder</heading>
      <body>Don't forget me this weekend!</body>
    </note>
    `;

    const base64EncodedXml = Buffer.from(content).toString('base64');

    detectBodyMime(base64EncodedXml,(err,mime,extention)=>{
        expect(['application/xml','text/xml']).toContain(mime);
        expect(extention).toBe('xml');
        expect(err).toBe(null);

        done();
    });
})


test("form url encoded",(done)=>{
    const content = `val=21221`;

    detectBodyMime(content,(err,mime,extention)=>{
        expect('application/x-www-form-urlencoded').toEqual(mime);
        expect(extention).toBe(null);
        expect(err).toBe(null);

        done();
    });
});

test("form url encoded multiple",(done)=>{
    const content = `pleas=help&me=plz&var[]=true&var[2]=false&var['blahblah']=ipsum`;


    detectBodyMime(content,(err,mime,extention)=>{
        expect('application/x-www-form-urlencoded').toEqual(mime);
        expect(extention).toBe(null);
        expect(err).toBe(null);

        done();
    });
});

test("form url encoded multiple array",()=>{

    const content=`pleas=ContentDisposition%3A%20formdata%3B%20name%3D%22myfile%22%3B%20filename%3D%22exclude.txt%22&me=plz&var[]=true&var[2]=false&var['blahblah']=ipsum`
    
    detectBodyMime(content,(err,mime,extention)=>{
        expect('application/x-www-form-urlencoded').toEqual(mime);
        expect(extention).toBe(null);
        expect(err).toBe(null);

        done();
    });
})

test("wrong formurl encoded data to be  detected ad text/plain",(done)=>{
    const content = `dawdwqeewqwqeewqwqewqeqw&me=plz&*3032-vcsjmvar[]=true&var[2]=false&var['blahblah']=ipsum`;


    detectBodyMime(content,(err,mime,extention)=>{
        expect('text/plain').toEqual(mime);
        expect(extention).toBe('txt');
        expect(err).toBe(null);

        done();
    });
});


test("random text to be detected ad text/plain",(done)=>{
    const content = `bxuiqwbuwqpniqbnxiqwpxbipwqxbiwqpxbqwipbqip`;


    detectBodyMime(content,(err,mime,extention)=>{
        expect('text/plain').toEqual(mime);
        expect(extention).toBe('txt');
        expect(err).toBe(null);

        done();
    });
});

test("base64 text to be detected ad text/plain",(done)=>{
    const content = `bxuiqwbuwqpniqbnxiqwpxbipwqxbiwqpxbqwipbqip`;
    const contentBase64 = Buffer.from(content).toString('base64');

    detectBodyMime(contentBase64,(err,mime,extention)=>{
        expect('text/plain').toEqual(mime);
        expect(extention).toBe('txt');
        expect(err).toBe(null);

        done();
    });
});

Another simple usage example is using connect:

const connect = require('connect');
const {detectBodyMime} = require('../src/common/http_utils');

const app = connect();



app.use(function(req,res,next){
    req.id = 1;

    var body = [];

    req.on('data',(data) => body.push(data) );

    req.on('end',() => {

        body = Buffer.concat(body).toString();
        
        detectBodyMime(body,(err,mime,extention,buffer)=>{

            // Here I can either log, analyze or check for mime
            res.setHeader("Content-Type",'text/plain');
            res.writeHead(200,{'X-val':3});

            res.end("MIME: "+mime+"\n"+"Suggested file extention: "+extention+"\n");
        });

    });
});

// app.use(function(req,res){
//     console.log("id:",req.id);
//     res.setHeader("Content-Type",'text/plain');
//     res.writeHead(200,{'X-val':3});
//     res.end("jhello");
// });

app.listen(8090)

The idea is to have a generic http body content-type analysis tool. The goal is to use it in my project: https://github.com/pc-magas/http-manipulator

One of the goals of the project is to have an http analyser, that will help me distinguish what requests perform once I develop, analyze or reverse engineer web applications.

Sometimes I need to verify if content-type is mismatched. Many apps may not follow the Http guidelines and post whatever developer has thought.

Algorithmic approach

The idea behind this is to check whether the body matches a given content type of check for magic number; then for specific detected types, I perform further checks.

Current use is in this incoming request logger

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.